Introduction to Blazor Component Testing

chrissainty

Chris Sainty

Posted on December 29, 2019

Introduction to Blazor Component Testing

This post is part of the third annual C# advent. Two new posts are published every day between 1st December and 25th December.

Introduction to Blazor Component Testing

One significant area which has been lacking in Blazor is testing. There is a placeholder issue for it, but for a very long time, there wasn't any progress. This is because testing was out of scope for the first official release of Blazor - which shipped with .NET Core 3 back in September.

However, just before the release in August, Steve Sanderson published a blog post introducing a prototype unit testing library for Blazor components. Steve's prototype has the goal of getting the conversation going around testing in Blazor. I've wanted to test the library for a while, but I haven't had the chance until now.

Let's start by covering some of the high level questions about this prototype.

I want to stress this is a prototype library and there is ZERO support for it, there isn't even a NuGet package right now. Therefore, anything you read here can, and most likely will, change.

What types of testing does it cover?

There are various ways to test web applications but the two most common are unit tests and end-to-end tests (E2E tests).

Unit testing

Unit tests are lightweight and fast to run if written correctly, of course. They test small "units" of code in isolation. For example, given a method which takes two numbers and returns the sum of them. You could write a unit test which checks that if you provide the inputs 2 and 2 that the method returns 4. However, checks in isolation can also be their downfall. It's possible that various units of code could run fine in isolation, but when put together they don't quite match up, and errors can occur.

End to end testing

E2E tests can help to combat the issues with unit tests. You can test the whole application stack using E2E testing. E2E tests often use a headless browser to run tests which assert against the DOM. This is achieved using tools such as Selenium which drive a headless browser and provide an interface to access the HTML. The drawback of these type of tests is they're much more heavyweight. They're also known to be brittle and slow; it takes substantial effort to both make them reliable and keep them that way.

The best of both

What Steve's prototype library attempts to do is to bring the best of both of these testing approaches but without the drawbacks - sounds pretty good to me!

How does it work?

Steve's library supplies a TestHost which allows components to be mounted using a TestRenderer under the hood. It's compatible with traditional unit testing frameworks such as XUnit and NUnit and gives us a straightforward and clean way of writing tests. This is an example of what a test looks like using XUnit.

<!-- MyComponent.razor -->

<h1>Testing is awesome!</h1>

<!--kg-card-end: markdown--><!--kg-card-begin: markdown-->

public class MyComponentTests
{
    private TestHost _host = new TestHost();

    [Fact]
    public void MyBlazingUnitTest()
    {
        var component = _host.AddComponent<MyComponent>();

        Assert.Equal("Testing is awesome!", component.Find("h1").InnerText);
    }
}

We use CSS selectors to locate points of interest in the rendered output. In the test above, I'm locating the h1 tag and asserting that it contains the text "Testing is awesome!".

We can run this test like we would a traditional unit test using the Visual Studio Test Explorer.

Introduction to Blazor Component Testing

The tests also run exactly how you would expect when using a CI pipeline. Here's an example from Azure DevOps.

Introduction to Blazor Component Testing

Using the library

As I mentioned earlier, there isn't a NuGet package available for the library just yet. To use it you have to either download/clone/fork the repo from Steve's GitHub account. I've created a fork of it to my GitHub, I've also updated all the packages to the latest versions.

Once you have a copy of the code, you can open up the solution included with the repo. You'll find a sample application and some sample tests which give a few good examples of how to use the library.

You can play around with it from here if you just want to get to know it better. But I've wanted to get some tests added into my Blazored libraries for quite a while now. So I thought this would be the perfect opportunity to do that and see how the library works with a real-world project.

Testing Blazored Modal

We're going to start by adding some tests to Blazored Modal. This is a reasonably straightforward component which is controlled via a service. By calling a method on the service and passing different options or parameters, the modal is displayed in different configurations.

I've decided on two test groupings, display tests and modal options tests. I like to try and group my tests to make them easier to find and maintain.

To start, we'll add a copy of Steve's testing library to the solution and also a new XUnit test project called Blazored.Modal.Tests.

Introduction to Blazor Component Testing

Next, we need to add a reference to the testing library from the XUnit project and a reference to the Blazored.Modal project.

Introduction to Blazor Component Testing

We're going to create each of the test groupings I mentioned earlier as classes in the Blazored.Modal.Tests project. But so we don't have to duplicate boilerplate code we're going to create a test base class to encapsulate it.

public class TestBase
{
    protected TestHost _host;
    protected IModalService _modalService;

    public TestBase()
    {
        _host = new TestHost();
        _modalService = new ModalService();
        _host.AddService<IModalService>(_modalService);
    }
}

We start by creating a new TestHost instance, which is provided by Steve's testing library. As the modal component relies on an IModalService to function, we also need to add an instance of one to the TestHost's DI container. This is the place to replace any services with mocks if your components are using services which make external calls.

Now we have our TestBase sorted, let's get cracking with our display tests. Let's start by making sure that the modals initial state is not visible.

public class DisplayTests : TestBase
{
    [Fact]
    public void ModalIsNotVisibleByDefault()
    {
        var component = _host.AddComponent<BlazoredModal>();
        var modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");

        Assert.Null(modalContainer);
    }
}

In the test we're creating an instance of the BlazoredModal component via the TestHost. Once we have that instance, we can use it to look for particular state. In our case, we're checking that no element has the .blazored-modal-active CSS class, as it's this class which makes the modal visible.

We now need a test to make sure the modal becomes visible when we call the Show method on the IModalService.

[Fact]
public void ModalIsVisibleWhenShowCalled()
{
    var component = _host.AddComponent<BlazoredModal>();
    _modalService.Show<TestComponent>("");

    var modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");

    Assert.NotNull(modalContainer);
}

This time we're using the _modalService instance we setup in the TestBase. Once we've created the instance of our BlazoredModal component, we call the Show method on the service. We then look for an element which has the .blazored-modal-active CSS class and check it's not null.

If you're wondering about the TestComponent type in the Show call, it's a simple component I created to use for these tests and looks like this.

internal class TestComponent : ComponentBase
{
    public const string TitleText = "My Test Component";

    [CascadingParameter] public ModalParameters ModalParameters { get; set; }

    public string Title
    {
        get
        {
            var cascadedTitle = ModalParameters.TryGet<string>("Title");
            return string.IsNullOrWhiteSpace(cascadedTitle) ? TitleText : cascadedTitle;
        }
    }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        base.BuildRenderTree(builder);

        builder.OpenElement(1, "h1");
        builder.AddContent(2, Title);
        builder.CloseElement();
    }
}

We'll see this used more later on.

So far so good, now we are going to test cancelling and closing the modal. Here are the tests.

[Fact]
public void ModalHidesWhenCloseCalled()
{
    var component = _host.AddComponent<BlazoredModal>();
    _modalService.Show<TestComponent>("");
    var modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");

    Assert.NotNull(modalContainer);

    _modalService.Close(ModalResult.Ok("Ok"));
    modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");

    Assert.Null(modalContainer);
}

[Fact]
public void ModalHidesWhenCancelCalled()
{
    var component = _host.AddComponent<BlazoredModal>();
    _modalService.Show<TestComponent>("");
    var modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");

    Assert.NotNull(modalContainer);

    _modalService.Cancel();
    modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");

    Assert.Null(modalContainer);
}

These two tests are very similar, the only difference is the method we call on the modal service, either Close or Cancel. As you can see, I've got two Asserts in each test. The first one is asserting that the modal is visible, then, after the Cancel or Close methods are called, the second assert checks that it's not anymore.

That's it for the display tests. Let's move on and look at the modal options tests next. I'm not going to go through them all as I think things will get very repetitive, but I do want to look at two of them.

public class ModalOptionsTests : TestBase
{
    // Other tests omitted for brevity

    [Fact]
    public void ModalDisplaysCorrectContent()
    {
        var component = _host.AddComponent<BlazoredModal>();

        _modalService.Show<TestComponent>("");

        var content = component.Find("h1");

        Assert.Equal(content.InnerText, TestComponent.TitleText);
    }

    [Fact]
    public void ModalDisplaysCorrectContentWhenUsingModalParameters()
    {
        var testTitle = "Testing Components";
        var parameters = new ModalParameters();
        parameters.Add("Title", testTitle);

        var component = _host.AddComponent<BlazoredModal>();

        _modalService.Show<TestComponent>("", parameters);

        var content = component.Find("h1");

        Assert.Equal(content.InnerText, testTitle);
    }
}

The first test is checking that the correct content gets rendered by the modal component. In the test, we're checking that the content of the TestComponent (see code from earlier) is rendered correctly inside the modal. The TestComponent just contains a simple h1 tag which will display the string "My Test Component" by default.

The next test is checking that the component being displayed renders correctly based on a value passed using ModalParameters, which are passed into child components via a CascadingParameter. In this test, we're setting a Title parameter with the value "Testing Components". We then check to make sure the correct title is displayed by the TestComponent.

The reason I wanted to highlight these two tests is because they show the more E2E style of testing achievable with this library. In order to test the BlazoredModal component fully we need to make sure it interacts with other components in the right way. And this is a very common scenario when building component based UIs, testing in isolation here wouldn't give us the same amount of confidence as testing these components together.

You can view all the tests mentioned in this post at the Blazored Modal repo.

Summary

That's where we're going to leave things. We've managed to get setup with Steve's library and write some decent tests to check the functionality of Blazored Modal.

In this post, we've taken a look at how to test Blazor components using Steve Sanderson's prototype testing library. We looked at how to get up and running with the library before using it to write some tests for the Blazored Modal component library.

I hope you've found this post useful and if you have any feedback for Steve about his prototype, then please head over to the repo and open an issue.

💖 💪 🙅 🚩
chrissainty
Chris Sainty

Posted on December 29, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related