In this article, we are going to learn about unit testing a Blazor WebAssembly Project using the bUnit testing library.

If you find some of the concepts we are testing hard to understand, we have a great Blazor WebAssembly Series that is worth checking out.

To download the source code for this article, you can visit our GitHub repository.

With that, let’s start.

How to Test Blazor WebAssembly?

To test a Blazor component, we need to render the component with any relevant input required so we can inspect the rendered markup. Also, we want to trigger event handlers and life cycle methods, to allow us to assert that the component is behaving as we expect it to. All of this is available to us through the bUnit testing library.

It is important to note that dependencies such as CSS are not rendered when running unit tests. In order to verify that our component is styled correctly, this would require E2E (End-to-End) or Snapshot testing, which would render the component in a browser, along with any styling we have applied. We will only cover unit testing in this article.

As testing Blazor components is very similar to testing normal C# code, we can use testing frameworks that we are familiar with, such as xUnit, NUnit, or MSTest. We use these testing frameworks in conjunction with bUnit to allow us to write stable unit tests for our Blazor components.

How To Write bUnit Tests?

There are some slight differences in writing tests for our Blazor components compared to normal C# code. bUnit renders the components through the TestContext class, which provides us access to the rendered component instance, so that we can interact with the component.

We can write our unit tests in either .cs or .razor files. Writing tests in .razor files provide an easier way to declare components and HTML markup, without having to do any character escaping. However, we need to ensure that the test project is set to the Microsoft.NET.Sdk.Razor SDK type, otherwise the .razor files won’t compile. As more people are probably familiar with writing tests in .cs files, we are going to use this for the demo.

Create Blazor WebAssembly Project

Firstly, we create a Blazor WebAssembly project by using the template from Visual Studio or the dotnet new blazorwasm command. 

Next, let’s create our component in the Pages folder that we’ll use for testing:

<p>Hello from TestComponent</p>

@code {
}

We create a very simple component with a p element and some text, which is enough for us to write our first test.

Create xUnit Test Project

With our basic component created, let’s create a test project, by using the xUnit template provided by Visual Studio or the dotnet new xunit command. 

We need to remember to add the bunit NuGet package to this project, along with a reference to our Blazor WebAssembly project. Let’s write our first test to ensure the markup of our component matches what we expect.

Test Markup Matching

We’ll start by creating a class where we are going to write our tests:

public class TestComponentTests : TestContext
{
}

We inherit the TestContext class, which saves us from having to create a new one for each test to render our component.

With this, we are ready to write our first test:

[Fact]
public void TestComponent()
{
    var cut = RenderComponent<TestComponent>();

    cut.MarkupMatches("<p>Hello from TestComponent</p>");
}

First, we decorate the method with the xUnit Fact attribute, to let the test runner know this is a test method. Next, we render our TestComponent. Finally, we verify that the markup matches the expected string.

Add Parameters to Component

More often than not, our components require input, in the form of parameters, so let’s create a component with one:

<p>Message: @Message</p>

@code {
    [Parameter]
    public string Message { get; set; }
}

Now we have a Message parameter that we simply render in a p element.

Next, we’ll create a new test:

[Fact]
public void TestComponentWithParameter()
{
    var message = "Message from test";

    var cut = RenderComponent<TestComponentWithParameter>(parameters => parameters.Add(p => p.Message, message));

    cut.MarkupMatches($"<p>Message: {message}</p>");
}

bUnit provides us with a strongly typed builder that we can use to pass parameters to our component. We use the Add() method from the ComponentParameterCollectionBuilder class to select our Message parameter and pass in the message we defined in our test method. Again, we test that the markup matches, ensuring to include our new message text.

Trigger Event Handlers

Blazor allows us to bind to element event handlers, which we can also create tests for, so let’s create a new component that includes an event handler:

<p>Button clicked: @buttonClicked</p>

<button @onclick="OnClick">Click me</button>

@code {
    bool buttonClicked = false;
    
    void OnClick() => buttonClicked = true;
}

We create a simple onclick event handler for the button, that sets buttonClicked to true.

Next, we can write a new test method to test our event handler:

[Fact]
public void TestComponentWithEventHandler()
{
    var cut = RenderComponent<TestComponentWithEventHandler>();

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

    cut.Find("p").MarkupMatches("<p>Button clicked: True</p>");
}

First, we render our new component. Then, we use the IRenderedComponent to find our button element, which returns an Anglesharp Dom element. This allows us to call the Click() method, which executes our onclick event handler. Finally, we find our p element and verify the markup matches.

If our event handler makes use of the EventArgs parameter, we can optionally provide this information in our test.

Let’s refactor our component to use one of the MouseEventArgs properties:

<p>Control key pressed: @controlKeyPressed</p>

<button @onclick="OnClick">Click me</button>

@code {

    bool controlKeyPressed = false;

    void OnClick(MouseEventArgs e) => controlKeyPressed = e.CtrlKey;
}

This time, instead of unconditionally setting our variable to true, we bind it to the CtrlKey property, which is set to true if the Ctrl key is pressed when clicking the button.

We must also change our test to account for this new functionality:

[Fact]
public void TestComponentWithEventHandler()
{
    var cut = RenderComponent<TestComponentWithEventHandler>();

    cut.Find("button").Click(ctrlKey: true);

    cut.Find("p").MarkupMatches("<p>Control key pressed: True</p>");
}

This time, we pass the ctrlKey optional parameter to the Click() method. We must also remember to update the MarkupMatches parameter as our markup has changed.

Service Injection

It is good practice to keep our components free from complex logic by using services and injecting them into the components that require them. bUnit provides us with the Services collection, so we can register any dependencies our component might need when writing our tests.

Let’s create a simple service interface to return some data:

public interface IDataService()
{
    List<string> GetData();
}

And a class that implements this interface:

public class DataService : IDataService
{
    public List<string> GetData() => new List<string> { "Data 1", "Data 2" };
}

With this service in place, let’s create a component that uses it:

@inject IDataService DataService;

@if (MyData is null)
{
    <p>Retrieving data...</p>
}
else
{
    <p>Data retrieved</p>
}

@code {

    public List<string> MyData { get; set; }

    protected override void OnInitialized()
    {
        MyData = DataService.GetData();
    }
}

First, we inject IDataService, and conditionally render a p element depending on whether MyData is null. In the OnInitialized life cycle method, we retrieve the data from our service.

Now we can write a test to verify MyData is populated correctly:

[Fact]
public void TestComponentWithInjection()
{
    Services.AddSingleton<IDataService, DataService>();

    var cut = RenderComponent<TestComponentWithInjection>();

    Assert.NotNull(cut.Instance.MyData);
}

The first thing we must do is register our service, using the Services collection. Next, we render our component, and finally, we assert that MyData is not null, by accessing the component under test Instance property.

Test JSInterop

When we build applications with Blazor, we often need to interact with JavaScript using the JSInterop. With bUnit, functionality is provided to emulate the IJSRuntime that we inject into our components to interact with JavaScript.

Let’s start by creating a component that invokes some JavaScript:

@inject IJSRuntime JSRuntime

<button @onclick="ShowAlert">Show Alert</button>

@code {
    private async Task ShowAlert() => await JSRuntime.InvokeVoidAsync("alert", "Alert from Blazor component");
}

First, we inject the IJSRuntime. Then we create a button that has an onclick event handler that calls the ShowAlert method. In this method, we invoke the JavaScript alert method, passing in a message to display.

Now we can write a test for this component: 

[Fact]
public void TestComponentWithJSInterop()
{
    JSInterop.SetupVoid("alert", "Alert from Blazor component");

    var cut = RenderComponent<TestComponentWithJSInterop>();

    cut.Find("button").Click();
    
    JSInterop.VerifyInvoke("alert", calledTimes: 1);
}

First, we need to set up the bUnit JSInterop, by calling the SetupVoid() method, passing in our alert method and message we want to show. Next, we render our component, and then find our button and call the Click() method. Finally, we verify our alert method was called once through the JSInterop.

Mock HttpClient

As Blazor WebAssembly is a purely client-side framework, we often need to interact with a server-side API over HTTP, which we do by using the HttpClient.

Let’s create a component and inject HttpClient:

@inject HttpClient Http;

@if (DataFromApi is null)
{
    <p>Retrieving data from API...</p>
}
else
{
    <p>Data from API retrieved</p>
}

@code {
    public List<string> DataFromApi { get; set; }

    protected override async Task OnInitializedAsync()
    {
  	DataFromApi = await Http.GetFromJsonAsync<List<string>>("/api/data");
    }
}

First of all, we inject our HttpClient. Similar to our component that injects a service, we conditionally render a p element depending on whether DataFromApi is null or not. In the OnInitializedAsync method, we retrieve our data from the /api/data endpoint.

Now we can write a unit test for this component. As we are injecting an HttpClient, we need to mock it, which we can do using the RichardSzalay.MockHttp package. This is not the only way to achieve this as we could create our own mock of HttpClient using a library such as Moq. However, we will only focus on the RichardSzalay package for this article.

With this package installed, let’s write our test:

[Fact]
public void TestComponentWithHttpClient()
{
    var content = JsonSerializer.Serialize(new List<string> { "data" });
    
    var mockHttp = new MockHttpMessageHandler();
    var httpClient = mockHttp.ToHttpClient();
    httpClient.BaseAddress = new Uri("http://localhost");

    Services.AddSingleton(httpClient);

    mockHttp.When("/api/data")
            .Respond(HttpStatusCode.OK, "application/json", content);

    var cut = RenderComponent<TestComponentWithHttpClient>();

    cut.WaitForAssertion(() => Assert.NotNull(cut.Instance.DataFromApi));
}

The first thing we do is create some JSON data that is going to be returned from our mocked HttpClient. Next, we create a new MockHttpMessageHandler and then call the ToHttpClient method to get our HttpClient, also making sure to set the BaseAddress. Then, we register the HttpClient with the Services collection. 

Now we need to set up the MockHttpMessageHandler to respond to requests to /api/data and return our JSON data. As usual, we register our component, and finally, as we are using an asynchronous method to retrieve our data, we need to use WaitForAssertion(), checking that our DataFromApi list is not null.

Mock NavigationManager with bUnit

Blazor provides a NavigationManager service, which can be injected into our component to give us browser navigation. bUnit provides a fake version of NavigationManager which is added by default to the TestContext.Services collection.

Let’s create a component that uses the NavigationManager:

@inject NavigationManager NavigationManager

<button @onclick="NavigateToHome">Navigate to Home</button>

@code {
    void NavigateToHome() => NavigationManager.NavigateTo("/home");
}

First, we inject the NavigationManager class and add a button with an onclick handler. In our NavigateToHome method, we simply call NavigateTo and navigate to /home.

Now we can create a test to ensure our component correctly invokes the NavigationManager class:

[Fact]
public void TestComponentWithNavigationManager()
{
    var navigationManager = Services.GetRequiredService<FakeNavigationManager>();
    var cut = RenderComponent<TestComponentWithNavigationManager>();

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

    Assert.Equal($"{navigationManager.BaseUri}home", navigationManager.Uri);
}

Firstly, we want to get the FakeNavigationManager provided by bUnit. Next, we render our component and find our button so we can execute the Click event. To test that our navigation worked correctly, we can compare our expected Uri of http://localhost/home to the NavigationManager.Uri property.

Conclusion

Now we have a good understanding of how to write unit tests for our Blazor components with bUnit. Unit testing gives us the confidence that our code does what we expect it to, and allows us to safely refactor code, knowing that we haven’t broken any piece in the process.