Testing Blazor components using bUnit
bUnit is a framework to test Blazor components. It allows validating that a component renders the expected HTML and reacts to events. bUnit runs in-memory and doesn't require a browser. This means tests run in isolation and are fast. Because there is no browser, it cannot execute JavaScript code. So, you may not be able to test correctly components that rely on JS interop. If your JS is very simple and does impact the behavior of the component it is possible to mock the IJSRuntime
interface as we'll see later. For more complex scenarios, you'll need to do end-to-end (E2E) testing with solutions such as Selenium, Puppeteer, or Playwright as explained in the previous post.
#bUnit basics
Let's create a component to test. It is based on the Counter.razor
component from the template:
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount += Increment;
}
}
Now you can create a unit test project that uses xUnit using Visual Studio or the command line:
dotnet new xunit -o MyTestProject
Then, add a reference to the project that contains the component, and add the NuGet packages bunit.web
and bunit.xunit
. The csproj file should look like:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="bunit.web" Version="1.0.0-beta-10" />
<PackageReference Include="bunit.xunit" Version="1.0.0-beta-10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyProject\MyProject.csproj" />
</ItemGroup>
</Project>
You can now create the first test:
[Fact]
public void CounterComponentTest()
{
// Arrange
using var ctx = new TestContext();
var component = ctx.RenderComponent<Counter>();
// Act
component.Find("button").Click();
// Assert
component.MarkupMatches(@"<p>Current count: 1</p><button>Click me</button>");
}
#Testing component parameters
Let's improve the previous sample with a parameter.
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
[Parameter]
public int Increment { get; set; } = 1;
private void IncrementCount()
{
currentCount += Increment;
}
}
You can set the parameter values using the SetParametersAndRender
method.
[Fact]
public void CounterComponentTest()
{
// Arrange
using var ctx = new TestContext();
var component = ctx.RenderComponent<Counter>();
component.SetParametersAndRender(parameters => parameters.Add(p => p.Increment, 2));
// Act
var button = component.Find("button");
button.Click();
button.Click();
// Assert: Count should be 4
component.MarkupMatches(@"<p>Current count: 4</p><button>Click me</button>");
}
#Testing asynchronous events
If you have an asynchronous event handler, you have to wait for the event to be complete before validating the result. bUnit provides a way to do that using WaitForAssertion
. This method retries the assertion until it passes.
Let's modify the component to use an asynchronous event handler:
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private async Task IncrementCount()
{
await Task.Delay(500);
currentCount += 1;
}
}
Now, we can update the test to use component.WaitForAssertion
. This way the test will wait until the event handler is completed.
[Fact]
public void CounterComponentTest()
{
// Arrange
using var ctx = new TestContext();
var component = ctx.RenderComponent<Counter>();
// Act
var btn = component.Find("button");
btn.Click();
// Assert
component.WaitForAssertion(() => component.MarkupMatches(@"<p>Current count: 1</p><button>Click me</button>"), timeout: TimeSpan.FromSeconds(2));
}
#Mock services
If your component uses Dependency Injection (DI) to inject services, you can mock them in the tests using the TestContext
:
@inject IClock SystemTime
<p>@SystemTime.Now().ToString("HH:mm")</p>
[Fact]
public void ClockComponentTest()
{
// Arrange
var clockFake = A.Fake<IClock>();
A.CallTo(() => clockFake.Now()).Returns(new DateTimeOffset(2020, 09, 01, 21, 42, 0, TimeSpan.Zero));
using var ctx = new TestContext();
ctx.Services.AddSingleton(clockFake);
// Act
var component = ctx.RenderComponent<Clock>();
// Assert
component.WaitForAssertion(() => component.MarkupMatches(@"<p>21:42</p>"), timeout: TimeSpan.FromSeconds(2));
}
#Mock IJSRuntime
Even if bUnit doesn't allow to run JS code, you can mock the IJSRuntime
interface. bUnit provides everything needed to mock this interface, so you don't need to use a mock framework.
Let's consider this component:
@inject IJSRuntime JSRuntime
<button @onclick="OnClick">Alert</button>
@code {
private async Task OnClick()
{
await JSRuntime.InvokeVoidAsync("alert", "Sample message");
}
}
[Fact]
public void JSRuntimeComponentTest()
{
// Arrange
using var ctx = new TestContext();
var mockJS = ctx.Services.AddMockJSRuntime(JSRuntimeMockMode.Strict);
mockJS.SetupVoid("alert", "Sample message");
// Can use mockJS.SetupVoid("alert", argumentsMatcher: _ => true); to match any parameter
var component = ctx.RenderComponent<SampleJSRuntime>();
// Act
component.Find("button").Click();
// Assert
mockJS.VerifyInvoke("alert", calledTimes: 1);
}
#Additional resources
- bUnit - a testing library for Blazor components
- Writing automated UI tests for an ASP.NET Core application using Playwright and xUnit
Do you have a question or a suggestion about this post? Contact me!