Twitter About Home

Unit testing Blazor components - a prototype

A possible approach for testing Blazor components that's as fast as unit testing and as high-level as browser automation

Published Aug 29, 2019

One of our design goals for Blazor is to offer an absolutely first-rate testing system. Writing tests for your components should be natural, produtive, and satisfying. Those tests should run fast and reliably. We’ve have a placeholder issue for this, but it’s out of scope for the initial (.NET Core 3.0) release.

So, I think it’s time to start clarifying how this can work.

Goals

We do have a lot of experience writing tests for Blazor components. While building the framework itself, a substantial portion of the code we write is:

  • Unit tests, which directly execute methods on components and renderers, and make assertions about the resulting render batches and UI diffs inside them. These tests are extremely fast and robust, but unfortunately also extremely low-level. You wouldn’t normally want to express your tests in terms of Blazor’s low-level internal UI diff description format.

  • End-to-end (E2E) tests, which use Selenium to control a headless browser instance and make assertions about the state of the browser DOM. These tests are high-level (in that they talk about familiar concepts like HTML and actions like clicking), but they are also very slow (each one can take multiple seconds to run) and great care has to be taken to make them robust and not depend on the timings of async UI updates. It’s also not really possible to mock external services in these tests.

We think it will be possible for Blazor to include a set of test helpers that give you the benefits of both approaches, and the drawbacks of neither.

The core idea is simply to provide unit test helpers that let you mount your components inside a “test renderer”. This will behave like browser automation, but no actual browser will be involved (not even a headless one). It’s just pure .NET code, so runs with the speed and robustness of pure unit tests.

A prototype

To get the conversation going, I’ve put a prototype here. Let’s take a look.

Consider the classic “counter” example, in a file called Counter.razor:

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Increment</button>

@code {
    int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

My prototype unit test package, Microsoft.AspNetCore.Components.Testing, provides a class TestHost that lets you mount components and interact with them inside any traditional unit testing system (e.g., xUnit, NUnit). So if your unit test project has a reference to the application project, you can write tests as follows:

public class CounterTest
{
    private TestHost host = new TestHost();

    [Fact]
    public void CountStartsAtZero()
    {
        var component = host.AddComponent<Counter>();
        Assert.Equal("Current count: 0", component.Find("p").InnerText);
    }

    [Fact]
    public void CanIncrementCount()
    {
        var component = host.AddComponent<Counter>();

        component.Find("button").Click();

        Assert.Equal("Current count: 1", component.Find("p").InnerText);
    }
}

As you can see, TestHost is a way to render components under unit tests. You can find rendered elements in terms of CSS selectors, then you can either make assertions about them or trigger events on them. This is very much like traditional browser automation, but is implemented without using any actual browser.

TestHost also provides a way to supply DI services, such as mock instances, so you can describe how your component must behave when external services do certain things (e.g., when authentication or data access fails). As an example of that, consider the classic “weather forecasts” page (see FetchData.razor in any brand-new Blazor project). This uses HttpClient to fetch and display data from an external source. You could write a unit test for that as follows:

public class FetchDataTest
{
    private TestHost host = new TestHost();

    [Fact]
    public void DisplaysLoadingStateThenRendersSuppliedData()
    {
        // Set up a mock HttpClient that we'll be able to use to test arbitrary responses
        var req = host.AddMockHttp().Capture("/sample-data/weather.json");

        // Initially shows loading state
        var component = host.AddComponent<FetchData>();
        Assert.Contains("Loading...", component.GetMarkup());

        // Now simulate a response from the HttpClient
        host.WaitForNextRender(() => req.SetResult(new[]
        {
            new FetchData.WeatherForecast { Summary = "First" },
            new FetchData.WeatherForecast { Summary = "Second" },
        }));

        // Now we should be displaying the data
        Assert.DoesNotContain("Loading...", component.GetMarkup());
        Assert.Collection(component.FindAll("tbody tr"),
            row => Assert.Contains("First", row.OuterHtml),
            row => Assert.Contains("Second", row.OuterHtml));
    }
}

Let’s do some TDD

You probably know this, but to recap: Test-driven Development (TDD) is the process of writing software through tests. For each behavior your software should have, you first write a unit test (which fails, because your software doesn’t have that behavior yet), then you implement the behavior, and then you see that your test now passes.

Whether or not TDD really works depends a lot on whether the type of software you’re writing is amenable to unit testing and whether the technologies you’re using give you good ways of isolating units of code and a fast enough feedback loop.

My hope, then, is that Blazor can offer unit test helpers that give you what you need so that TDD is not only viable but even enjoyable, even when writing components with very detailed user interactions.

As an experiment, pretend you’re building a “todo list” component in Blazor. It needs to let users type in items, add them to a list, and check them off when they are done. Pretty obvious stuff. You might start with the following TodoList.razor component:

@page "/todo"

<h1>Todo</h1>
<ul>
    @foreach (var item in items)
    {
        <li>@item.Text</li>
    }
</ul>

<form>
    <input placeholder="Type something..." />
    <button type="submit">Add</button>
</form>

@code {
    private List<TodoItem> items = new List<TodoItem>();

    class TodoItem
    {
        public string Text { get; set; }
        public bool IsDone { get; set; }
    }
}

Initial todo list

Currently, it doesn’t do anything. Typing and clicking “Add” does nothing. Let’s write a unit test describing the behavior we want. This would go into a separate unit testing project that references the app project:

public class TodoListTest
{
    private TestHost host = new TestHost();

    [Fact]
    public void InitiallyDisplaysNoItems()
    {
        var component = host.AddComponent<TodoList>();
        var items = component.FindAll("li");
        Assert.Empty(items);
    }

    [Fact]
    public void CanAddItem()
    {
        // Arrange
        var component = host.AddComponent<TodoList>();

        // Act
        component.Find("input").Change("My super item");
        component.Find("form").Submit();

        // Assert
        Assert.Collection(component.FindAll("li"),
            li => Assert.Equal("My super item", li.InnerText.Trim()));
    }
}

If you run this either on the command line with dotnet test or in VS’s built-in runner, you’ll see that InitiallyDisplaysNoItems passes, but CanAddItem fails (because we haven’t yet implemented it):

First test failure

The actual reported failure comes from our call to .Change - it reports The element does not have an event handler for the event ‘onchange’.. This makes sense because we’re trying to trigger “change” on the input, but there’s nothing bound to that element.

Let’s implement the adding behavior, by using @bind to wire up a “change” listener on the input, plus an @onsubmit listener on the form that actually adds items:

<form @onsubmit="AddItem">
    <input placeholder="Type something..." @bind="newItemText" />
    <button type="submit">Add</button>
</form>

@code {
    private string newItemText;
    private List<TodoItem> items = new List<TodoItem>();

    void AddItem()
    {
        items.Add(new TodoItem { Text = newItemText });
    }

    class TodoItem { /* Leave as before */ }
}

And now, the test passes:

First test pass

OK, that’s good, but I just remembered something else: each time you submit an item, we should auto-clear out the textbox so you can type in the next one. To see whether this already happens, try updating the unit test to check the textbox becomes empty:

[Fact]
public void CanAddItem()
{
    // ... leave the existing code here, and just add the following ...
    Assert.Empty(component.Find("input").GetAttributeValue("value", ""));
}

Oops, no - now this test fails with Assert.Empty() Failure. This shows we’re not clearing out the textbox after add. To implement this, update the AddItem method:

void AddItem()
{
    items.Add(new TodoItem { Text = newItemText });
    newItemText = null;
}

… and now the test passes. We’ve implemented the basics of adding items:

Adding todo items

Now, what about having checkboxes to mark items as done, and counting the number of remaining items? We could express that in a unit test by saying there must be an element with CSS class .remaining that shows the count, and that each li should contain a checkbox you can toggle:

[Fact]
public void ShowsCountOfRemainingItems()
{
    // Arrange: list with two items
    var component = host.AddComponent<TodoList>();
    component.Find("input").Change("Item 1");
    component.Find("form").Submit();
    component.Find("input").Change("Item 2");
    component.Find("form").Submit();

    // Assert 1: shows initial 'remaining' count
    Assert.Equal("2", component.Find(".remaining").InnerText);

    // Act/Assert 2: updates count when items are checked
    component.Find("li:first-child input[type=checkbox]").Change(true);
    Assert.Equal("1", component.Find(".remaining").InnerText);
}

Of course, this initially fails, because there is no .remaining item. But we can implement the behavior by changing the markup in TodoList.razor:

<h1>Todo (<span class="remaining">@items.Count(x => !x.IsDone)</span>)</h1>
<ul>
    @foreach (var item in items)
    {
        <li>
            <input type="checkbox" @bind="item.IsDone" />
            @item.Text
        </li>
    }
</ul>

… and now the test passes. Not surprisingly, if you run the app in an actual browser, you can also see it working:

Toggling todo items

Status

So far, this is just a prototype, and we don’t plan to ship anything of this kind in the 3.0 release this year. My expectation is that we’ll be looking for your feedback as this prototype evolves over time. Maybe the final result will look just like this, or maybe it will be unrecognisably different in the end.

At some point I expect we will ship unit test helpers in a preview package so you can get started using them for real, and ultimately have them in the box with the 5.0 release in late 2020.

READ NEXT

IndexedDB in Blazor

Exploring the Reshiru.Blazor.IndexedDB package

Published Aug 3, 2019