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.
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.