Unit Testing Blazor Components with xUnit and bUnit
Setting Up a Unit Test Project for Blazor
Let's create unit tests for a Blazor application. In the provided code sample, you'll find a test solution. Open it with your preferred editor. The solution should look familiar, containing Counter and FetchData components that use IWeatherService to retrieve weather forecasts from a server.
Adding a Unit Test Project
We'll use xUnit, a popular .NET testing framework, to test our Blazor components.
In Visual Studio, right-click the test folder and select Add > New Project. In the Add New Project dialog, search for the "xUnit Test Project" template and click Next. Set the Location to the test folder and name it Testing.ComponentTests.
If using the command line, navigate to the test folder and execute:
dotnet new xunit -n Testing.ComponentTests
Then change to the parent directory and run:
dotnet sln add .\test\Testing.ComponentTests
Regardless of your development tool, add project references to both the client and shared projects. The test project file should look like this, with nullable reference types enabled:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<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>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Client\Testing.Client.csproj" />
<ProjectReference Include="..\..\src\Shared\Testing.Shared.csproj" />
</ItemGroup>
</Project>
Adding bUnit to the Test Project
With the current unit test project, we can test services and other non-Blazor classes. To test Blazor components, we need to add bUnit. Add the bUnit package using your preferred method (select the latest stable version).
You'll also need to change the project SDK as shown below. We need to do this because we'll use Razor syntax to build unit tests for Blazor components.
<Project Sdk="Microsoft.NET.Sdk.Razor">
Writing Your First Unit Test
Now that everything is set up, we can write our first unit test. We'll start with a simple test to see how it works with Visual Studio and VS Code.
Writing Good Unit Test Methods
Each unit test follows three stages: Arrange, Act, and Assert, also known as the three A's of unit testing. The Arrange phase sets up the unit test by creating the System Under Test (SUT), which means the class we're testing and its dependencies. The Act phase performs the call to the method we're testing, and Assert verifies that the result is successful.
Add a new class named MathHelper to the Shared project as shown below. The Square method should return the square of a number (and it has a bug).
namespace Testing.Shared
{
public class MathHelper
{
public int Square(int number)
{
return number;
}
}
}
Let's write a simple unit test for this method as shown below. For xUnit, a unit test is a public method with the [Fact] attribute. As this attribute suggests, the test's outcome should be a fact! In the Arrange phase, we set up what I like to call the sut (System Under Test). This makes it easy for me to identify the instance I want to test (just a convention, feel free to name it differently). Then in the Act phase, we call the Square method and store the result in an actual variable. Next comes the Assert phase, where I use the Assert clas from xUnit to verify that the result matches the expected outcome. The Assert class has a series of methods to check if the test's outcome is as expected. Here, we use the Equals method to see if the result equals 9, which should be the square of 3.
using Testing.Shared;
using Xunit;
namespace Testing.ComponentTests
{
public class SquareShould
{
[Fact]
public void Return9For3()
{
// Arrange
var calculator = new MathHelper();
// Act
var result = calculator.Square(3);
// Assert
Assert.Equal(expected: 9, actual: result);
}
}
}
Running Your Tests
In Visual Studio, open the Test Explorer window (Test > Test Explorer). The Test Explorer is where you run unit tests and view results. After opening Test Explorer, it will scan your solution for unit tests and list them. Now click the green arrow button in this window to run all tests.
The tests will run and fail. You can also run unit tests from Visual Studio Code, but you need to install the .NET Core Test Explorer extension.
Now you can run tests by clicking the Test Explorer icon on the left side of VSC. The VSC Test Explorer will show several buttons. From left to right, you have buttons to run tests, refresh the list of available tests, stop test execution, and show logs with test results.
Making Your Tests Pass
Why did the test fail? If you place a breakpoint in the Square method and click the arrow in Test Explorer again, you'll see that Visual Studio doesn't stop at your breakpoint. The same is true for VSC. Why? Debugging requires some special settings that take time. Remember we want our tests to complete as quickly as possible?
Fix the Square method as shown below:
namespace Testing.Shared
{
public class MathHelper
{
public int Square(int number)
{
return number * number;
}
}
}
Now run the tests again (with or without the debugger). They should pass.
Using Facts and Theories
But what about other values? With xUnit, we can write a whole suite of tests without copying and pasting a lot of test code (copy-pasting into duplicate code is generally bad, also known as Don't Repeat Yourself or DRY). Add another unit test to the SquareShould class as shown below. Here, we use the [Theory] attribute to tell xUnit to run it with different parameters. We use the [InlineData] attribute to pass parameters to the test method.
[Theory]
[InlineData(1, 1)]
[InlineData(2, 4)]
[InlineData(-1, 1)]
public void ReturnSquareOfNumber(int input, int expectedOutput)
{
// Arrange
var calculator = new MathHelper();
// Act
var result = calculator.Square(input);
// Assert
Assert.Equal(expected: expectedOutput, actual: result);
}
When we run the tests now, you'll see xUnit running three tests, one for each [InlineData] attribute.
Checking Your Assumptions
Do you have a piece of code that behaves differently than you expected? Personally, I start questioning my sanity, like "Am I going crazy?" Or are you using someone's method that is poorly documented and doesn't work as it should? With unit tests, you can set up checks to see if a method does what you think it should do. If not, maybe you need to talk to the author to see what makes more sense. When you have unit tests, you can attach them to bug reports, making it easy for the author to reproduce the issue.
Let's look at another example. Now I want to see if passing a large integer to the Square method throws an error (not every squared integer fits in another integer due to its limited range). Add another test method as shown below. So here we call Square with the maximum int. The result can never fit in an int, so we expect this to throw an OverflowException.
[Fact]
public void ThrowOverflowForBigNumbers()
{
// Arrange
var calculator = new MathHelper();
// Act & Assert
Assert.Throws<OverflowException>(() =>
{
int result = calculator.Square(int.MaxValue);
});
}
Why does it fail? Let's set a breakpoint on the Square method. Maybe we're doing something wrong here? Run the test with the debugger. When the debugger stops, look at the parameter's value: 2147483647. This is the maximum signed integer. Now exit the method until the result is set. What's its value? It's 1. Now 2147483647 * 2147483647 is not 1! What's going on? It turns out that C# works like C++ and C. These programming languages don't throw exceptions on overflow by default! They even use this to create hashing and encryption algorithms.
So how do we solve this? You can use the checked keyword in C# to turn on overflow checking as shown below:
namespace Testing.Shared
{
public class MathHelper
{
public int Square(int number)
{
checked
{
return number * number;
}
}
}
}
Run the tests again. Now they pass. Wow! That's actually the normal behavior. Unit tests are great for discovering these kinds of odd behaviors and allowing you to catch changes that would later cause bugs.
Writing bUnit Tests in C#
We've learned how to write unit tests for .NET classes and their methods. Here, we'll learn how to write tests for Blazor components on top of xUnit.
Understanding bUnit
bUnit is a testing library for Blazor components written by Egil Hansen, with source code available on GitHub at https://github.com/bUnit-dev/bUnit. With bUnit, you can easily write unit tests for Blazor components. Why should we write unit tests for Blazor components? The reasons are the same as for regular classes: to ensure they work as expected and to ensure they continue to work when some dependencies are updated. Of course, most of your tests should be on service classes that implement business logic. For example, you want to ensure that your Blazor component calls a method on that service when the user interacts with the component. With bUnit, we can automate this, so the user doesn't actually need to click the button! We can run these tests continuously, so we know within minutes after a change when we've broken a component.
Testing a Blazor component involves rendering and checking its output. But it's much more than that. You can interact with the component and see changes, replace dependencies, and more.
Let's start with the Counter component as shown below. This now-familiar component displays a currentCount field that starts at 0. So a very simple unit test would be to check if the component's output matches the expected output.
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">
Click me
</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
Add a new class named CounterShould to the unit test project. You can name this class anything you want, but I like the convention of using the method or component name followed by the word "should". Derive this class from the TestContext base class, which will give you access to all the convenient methods in bUnit. We'll continue to use these methods, and by deriving your test classes from TestContext, they can be acquired through inheritance.
Implement the first unit test RenderCorrectlyWithInitialZero as shown below:
using Bunit;
using Testing.Client.Pages;
using Xunit;
namespace Testing.ComponentTests
{
public class CounterShould : TestContext
{
[Fact]
public void RenderCorrectlyWithInitialZero()
{
var cut = RenderComponent<Counter>();
cut.MarkupMatches(@"
<h1>Counter</h1>
<p>Current count: 0</p>
<button class=""btn btn-primary"">
Click me
</button>");
}
}
}
We can do even better by focusing on the relevant parts of the component. We know our Counter component uses the <p> element to render the currentCount variable, but how do we access this part of the render tree? The bUnit library has a Find method that takes a CSS selector and returns the query result. Add another test method to the ShouldRender class as shown below. We find the <p> element and we can use the MarkupMatches method to see if it matches the expected output, which ignores whitespace.
[Fact]
public void RenderParagraphCorrectlyWithInitialZero()
{
var cut = RenderComponent<Counter>();
cut.Find(cssSelector: "p")
.MarkupMatches("<p>Current count: 0</p>");
}
Run your tests to see if they pass; they should pass.
Testing Component Interactions
Our Counter component has a button that, when clicked, should increment the currentCount by 1 and re-render with the new value. Let's see how we can test this by interacting with the Blazor component and see if it updates correctly. Add a new unit test to the ShouldRender class as shown below. The second line in the test uses the Find method to retrieve the button and then uses the Click method to perform its @onclick event. This should have the expected side effect, which we test in the next line to see if the component re-renders with the expected value. Run the test; it should pass. Hey, that was easy!
[Fact]
public void IncrementCounterWhenButtonIsClicked()
{
var cut = RenderComponent<Counter>();
cut.Find(cssSelector: "button")
.Click();
cut.Find(cssSelector: "p")
.MarkupMatches(@"<p>Current count: 1</p>");
}
The bUnit library comes with many dispatch methods that can trigger events on components. Use the Find method to retrieve an element in the component and then call the appropriate dispatch method on it, such as Click. These dispatch methods also allow you to pass event parameters. So let's look at an example.
First, add a new component called MouseTracker to your Blazor project using the markup below:
<div style="width: 300px; height: 300px;
background: green; margin:50px"
@onmousemove="MouseMove">
@position
</div>
This component has a MouseMove event handler as shown below:
using Microsoft.AspNetCore.Components.Web;
namespace Testing.Client.Pages
{
public partial class MouseTracker
{
private string position = "";
private void MouseMove(MouseEventArgs e)
=> position = $"Mouse at {e.ClientX}x{e.ClientY}";
}
}
Add a new class named MouseTrackerShould to your unit test project with a unit test as shown below. In the Arrange phase of the bUnit test, we create a MouseEventArgs instance and set ClientX and ClientY to some values. Then we use the RenderComponent method from TestContext to create an instance of the MouseTracker component. Now we find the div from the component and store it in theDiv reference.
Now we can perform the Act phase of the test by triggering the MouseMove event, passing the MouseMoveEventArgs instance we created earlier. This will re-render the component, so we're ready for the Assert phase, where we use the MarkupMatches method to check if theDiv has the expected content. Note that we're using semantic comparison again, and here we can use the style:ignore attribute to tell the comparison to also ignore style attributes. We'll discuss this in more detail later in the chapter.
using Bunit;
using Microsoft.AspNetCore.Components.Web;
using Xunit;
namespace Testing.ComponentTests
{
public class MouseTrackerShould : TestContext
{
[Fact]
public void ShowCorrectMousePosition()
{
var eventArgs = new MouseEventArgs()
{
ClientX = 100,
ClientY = 200
};
var cut = RenderComponent<MouseTracker>();
var theDiv = cut.Find(cssSelector: "div");
theDiv.MouseMove(eventArgs);
theDiv.MarkupMatches($"<div style:ignore>Mouse at {eventArgs.ClientX}x{eventArgs.ClientY}");
}
}
}
Passing Parameters to Our Components
Through data binding, we can pass parameters from a parent component to a child component. How do we pass parameters with bUnit? First, copy the Counter component in the Blazor project, rename it to TwoWayCounter, and change it as shown below. This TwoWayCounter component has several parameters, including CurrentCount and Increment.
<h1>Counter</h1>
<p>Current count: @CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
[Parameter]
public int CurrentCount
{
get => currentCount;
set
{
if (value != currentCount)
{
currentCount = value;
CurrentCountChanged.InvokeAsync(currentCount);
}
}
}
private int increment = 1;
[Parameter]
public int Increment
{
get => increment;
set
{
if(value != increment)
{
increment = value;
IncrementChanged.InvokeAsync(increment);
}
}
}
[Parameter]
public EventCallback<int> CurrentCountChanged { get; set; }
[Parameter]
public EventCallback<int> IncrementChanged { get; set; }
private void IncrementCount()
{
CurrentCount += Increment;
}
}
Add another unit test to the test project named TwoWayCounterShould and add the first bUnit test as shown below. We want to pass two parameters to this component, and we can do this by using an overload of the RenderComponent method as shown below. This takes a delegate with a parameter of type ComponentParameterCollectionBuilder<TComponent>. This class has an Add method with two parameters: where you pass the parameter name and the parameter value expression.
using Bunit;
using Testing.Client.Pages;
using Xunit;
namespace Testing.ComponentTests
{
public class TwoWayCounterShould : TestContext
{
[Fact]
public void IncrementCounterWhenClicked()
{
var cut = RenderComponent<TwoWayCounter>(
parameters =>
parameters.Add(counter => counter.CurrentCount, 0)
.Add(counter => counter.Increment, 1)
);
cut.Find("button").Click();
cut.Find("p")
.MarkupMatches("<p>Current count: 1</p>");
}
}
}
This way of passing parameters to components is very convenient because we can use IntelliSense to select the parameter's name. There are other ways to pass parameters, as shown below. Here, we use xUnit's Theory to pass different parameters to the component, and each parameter is passed as a ValueTuple containing the name and value of each parameter (which is why they're wrapped in parentheses).
However, I personally don't like this way of working because now we're passing the parameter's name as a string. Using hardcoded strings in code that contains clas names, properties, and other code structures is an anti-pattern I call "string-based programming" and should be avoided.
[Theory]
[InlineData(3)]
[InlineData(-3)]
public void IncrementCounterWithIncrementWhenClicked(int incrementAmount)
{
var cut = RenderComponent<TwoWayCounter>(
("CurrentCount", 0),
("Increment", incrementAmount)
);
cut.Find("button").Click();
cut.Find("p")
.MarkupMatches($"<p>Current count: {incrementAmount}</p>");
}
Of course, with modern C#, we can fix this and still use the style in the example below. Here, we use the nameof operator, which gets the property's name and returns its string representation. You can also use nameof with classes, methods, and other things.
[Theory]
[InlineData(3)]
[InlineData(-3)]
public void IncrementCounterWithIncrementWhenClickedWithNameOf(
int incrementAmount)
{
var cut = RenderComponent<TwoWayCounter>(
(nameof(TwoWayCounter.CurrentCount), 0),
(nameof(TwoWayCounter.Increment), incrementAmount)
);
cut.Find("button").Click();
cut.Find("p")
.MarkupMatches($"<p>Current count: {incrementAmount}</p>");
}
Testing Two-Way Data Binding and Events
Our TwoWayCounter has parameters that implement two-way data binding. Let's see if this component correctly implements this. We can use the same technique as before to pass handlers to the CurrentCountChanged and IncrementChanged parameters. But before we do that, add the FluentAssertions package to your test project. FluentAssertions allows you to write your assert statements in a more readable and concise way, which we'll use here (though it's not required).
Look at the bUnit test below. We've added four parameters, two of which are of type EventCallback<int>. We use delegates to assign to EventCallback<int>, which increment a local variable. This way, we count how many times the CurrentCountChanged and IncrementChanged event callbacks are called.
After clicking the button, we expect CurrentCountChanged to have been called, and we test this using the FluentAssertions Should().Be(1) method call. But we also want to test the Increment property's change handler, which we can do by accessing the component using the cut.Instance property and directly assigning a new value to Increment. If your compiler gives a warning on this statement, that's normal because usually you're not allowed to access a component's parameters directly from code.
[Fact]
public void TriggerChangedEventForCurrentCounter()
{
int currentCountChangedCount = 0;
int incrementChangedCount = 0;
var cut = RenderComponent<TwoWayCounter>(parameters =>
parameters.Add(counter => counter.CurrentCount, 0)
.Add(counter => counter.Increment, 1)
.Add(counter => counter.CurrentCountChanged,
() => currentCountChangedCount++)
.Add(counter => counter.IncrementChanged,
() => incrementChangedCount++)
);
cut.Find("button").Click();
cut.Instance.Increment = 2;
currentCountChangedCount.Should().Be(1);
incrementChangedCount.Should().Be(1);
}
You can also change parameter values after the component is first rendered. Look at the example below where we use the SetParametersAndRender method to modify the Increment parameter's value.
[Fact]
public void TriggerChangedEventForCurrentCounter2()
{
int incrementChangedCount = 0;
var cut = RenderComponent<TwoWayCounter>(parameters =>
parameters.Add(counter => counter.CurrentCount, 0)
.Add(counter => counter.Increment, 1)
.Add(counter => counter.IncrementChanged,
() => incrementChangedCount++)
);
cut.SetParametersAndRender(parameters =>
parameters.Add(counter => counter.Increment, 2));
incrementChangedCount.Should().Be(1);
}
Testing Components That Use RenderFragment
What about components that use RenderFragment (like ChildContent and templated components)? RenderFragment is a special Blazor type, so it requires special attention. First, add an Alert component to your Blazor project as shown below.
<div class="alert alert-secondary mt-4" role="alert">
@ChildContent
</div>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
}
Now add the AlertShould class to your test project as shown below. As you can see, ChildContent is just another parameter, but with some convenient methods to make it easy to add.
using Bunit;
using Testing.Client.Pages;
using Xunit;
namespace Testing.ComponentTests
{
public class AlertShould : TestContext
{
[Fact]
public void RenderSimpleChildContent()
{
var cut = RenderComponent<Alert>(parameters =>
parameters.AddChildContent("<p>Hello world!</p>"));
cut.MarkupMatches(@"
<div class=""alert alert-secondary mt-4"" role=""alert"">
<p>Hello world!</p>
</div>
");
}
}
}
If the Alert component has additional parameters, we can pass them as in the example above.
In the example below, we pass some simple HTML as ChildContent, but we can do more complex things. For example, in the example below, we pass a Counter as ChildContent.
[Fact]
public void RenderCounterAsChildContent()
{
var cut = RenderComponent<Alert>(parameters =>
parameters.AddChildContent<Counter>());
var p = cut.Find("p");
p.MarkupMatches("<p>Current count: 0</p>");
}
We can even pass parameters to ChildContent, for example, when using the TwoWayCounter as shown below.
[Fact]
public void RenderTwoWayCounterWithParametersAsChildContent()
{
var cut = RenderComponent<Alert>(parameters =>
parameters.AddChildContent<TwoWayCounter>(parameters =>
parameters.Add(counter=>counter.CurrentCount, 3)));
var p = cut.Find("p");
p.MarkupMatches("<p>Current count: 3</p>");
}
You can even call AddChildContent multiple times to add multiple fragments. The example below illustrates this, where we add both an HTML string and a counter. Also note the use of const strings, so we don't need to synchronize the content used in AddChildContent and MarkupMatches methods (Don't Repeat Yourself or DRY).
[Fact]
public void RenderTitleAndCounterAsChildContent()
{
const string header = "<h1>This is a counter</h1>";
var cut = RenderComponent<Alert>(parameters =>
parameters.AddChildContent(header)
.AddChildContent<Counter>());
var h1 = cut.Find("h1");
h1.MarkupMatches(header);
var p = cut.Find("p");
p.MarkupMatches("<p>Current count: 0</p>");
}
What About Templated Components?
First add (or copy from the book's provided code download) the templated component with markup and code below. This templated component uses two RenderFragments and one RenderFragment<TItem>. It also has a parameter to pass a Loader, which is a function that gets the items for the component. First, we'll look at RenderFragment, then we'll look at RenderFragment<TItem>.
@typeparam TItem
@if (items is null)
{
@LoadingContent
}
else if (items.Count() == 0)
{
@EmptyContent
}
else
{
<div class="list-group @ListGroupClass">
@foreach (var item in items)
{
<div class="list-group-item">
@ItemContent(item)
</div>
}
</div>
}
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace Testing.Client.Pages
{
public partial class TemplatedList<TItem>
{
IEnumerable<TItem>? items;
[Parameter]
public Func<ValueTask<IEnumerable<TItem>>>? Loader { get; set; }
[Parameter]
public RenderFragment LoadingContent { get; set; } = default!;
[Parameter]
public RenderFragment? EmptyContent { get; set; } = default!;
[Parameter]
public RenderFragment<TItem> ItemContent { get; set; } = default!;
[Parameter]
public string ListGroupClass { get; set; } = string.Empty;
protected override async Task OnParametersSetAsync()
{
if (Loader is not null)
{
items = await Loader();
}
}
}
}
Add the TemplatedListShould class to the test project as shown below. Here, we add two parameters, one for the Loader parameter and one for the LoadingContent template. As you can see, we can use the same Add method as for normal parameters.
using Bunit;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Testing.Client.Pages;
using Xunit;
namespace Testing.ComponentTests
{
public class TemplatedListShould : TestContext
{
[Fact]
public void RenderLoadingTemplateWhenItemsIsNull()
{
const string loading =
"<div class=\"loader\">Loading...</div>";
Func<ValueTask<IEnumerable<string>?>> loader =
() => new ValueTask<IEnumerable<string>?>(result: null);
var cut = RenderComponent<TemplatedList<string>>(
parameters =>
parameters.Add(tl => tl.Loader, loader)
.Add(tl => tl.LoadingContent, loading)
);
cut.Find("div.loader")
.MarkupMatches(loading);
}
}
}
But what about using the more complex RenderFragment<TItem> for the ItemContent parameter? Add a new unit test as shown below. Here, we'll pass five strings using a loader Func<ValueTask<IEnumerable<string>>>. Note the use of the Enumerable.Repeat method to create a collection of elements. We pass the loader as a parameter to the TemplatedList<string> component, and we also pass the ItemContent, which is a RenderFragment<string>. Because this takes a parameter, we use a Func<string, string> delegate that will return a RenderFragment<string> (because the Add method will handle this).
Now we want to check if it uses the ItemContent for each item in our collection (five "A" strings). There's a FindAll method that takes a CSS selector, which will return all elements that match the selector. The ItemContent RenderFragment uses a p, so we use that as the CSS selector. First, we check that the number of paragraphs matches the number of items, and then we iterate through each item and check that the markup matches the expected output.
[Fact]
public void RenderItemsCorrectly()
{
const int count = 5;
Func<ValueTask<IEnumerable<string>>> loader =
() => new ValueTask<IEnumerable<string>>(
Enumerable.Repeat("A", count));
var cut = RenderComponent<TemplatedList<string>>(
parameters =>
parameters.Add(tl => tl.Loader, loader)
.Add(tl => tl.ItemContent,
(context) => $"<p>{context}</p>"));
var ps = cut.FindAll("p");
ps.Should().NotBeEmpty();
foreach (var p in ps)
{
p.MarkupMatches("<p>A</p>");
}
}
Run this test; it should usually pass. If it doesn't, we'll discuss this in the "Handling Asynchronous Re-rendering" section, so please continue reading.
One last example. Let's use another component as ItemContent and pass the context as a parameter. Add a new component called ListItem below (it's a copy-paste of the ItemContent from the example above).
<p>@Item</p>
@code {
[Parameter]
public string Item { get; set; } = default!;
}
Now copy and paste the RenderItemsCorrectly method, rename it to the example below. The only part that needs modification in this list is where we pass the ItemContent parameter. If you want to pass a component as RenderFragment<TItem>, you need to use the Add<ComponentType, TItem> overload, where the first generic parameter is the type of the component to use, and the second is the type of the component to use for the RenderFragment<TItem>. So in this specific case, ComponentType is ListItem, and TItem is string (because we're passing IEnumerable<string> to TemplatedList).
[Fact]
public void RenderItemsWithListItemCorrectly()
{
const int count = 5;
Func<ValueTask<IEnumerable<string>?>> loader =
() => new ValueTask<IEnumerable<string>?>(
Enumerable.Repeat("A", count));
var cut = RenderComponent<TemplatedList<string>>(
parameters =>
parameters.Add(tl => tl.Loader, loader)
.Add<ListItem, string>(tl => tl.ItemContent,
context => itemParams
=> itemParams.Add(p => p.Item, context)
)
);
var ps = cut.FindAll("p");
ps.Should().NotBeEmpty();
foreach (var p in ps)
{
p.MarkupMatches("<p>A</p>");
}
}
This Add<ListItem, string> overload has two expressions: the first returns the parameter to set (ItemContent), and the second expression needs a bit more explanation. Let's look at this somewhat hard-to-read code:
Add<ListItem, string>(
tl => tl.ItemContent,
context => itemParams
=> itemParams.Add(p => p.Item, context)
)
So the first parameter is tl => tl.ItemContent, which returns the parameter to set. The second parameter is a lambda function that takes a value of TItem (in our case a string) and returns another lambda that takes a ComponentParameterCollectionBuilder<TComponent>. This sounds familiar? Yes. It's the same type we used at the beginning of this section to pass parameters to components (example above). Here, we're adding parameters to the ListItem component by calling Add.
Run this test (and the other tests if you want). All tests should pass. Phew!
Using Cascading Parameters
Some components use one or more cascading parameters, so to test these components, we need to pass values for the cascading parameters. First, copy the Counter component and rename it to CounterWithCV. Add an Increment cascading parameter as shown below.
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">
Click me
</button>
@code {
[CascadingParameter]
public int Increment { get; set; }
private int currentCount = 0;
private void IncrementCount()
{
currentCount += Increment;
}
}
Add a new test class named CounterWithCVShould and implement the test as shown below. As you can see, since cascading properties are identified by their type, you just need to pass the value.
using Bunit;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Testing.Client.Pages;
using Xunit;
namespace Testing.ComponentTests
{
public class CounterWithCVShould : TestContext
{
[Fact]
public void ShouldUseCascadingIncrement()
{
var cut = RenderComponent<CounterWithCV>(parameters =>
parameters.AddCascadingValue(3));
cut.Find(cssSelector: "button")
.Click();
cut.Find(cssSelector: "p")
.MarkupMatches(@"<p>Current count: 3</p>");
}
}
}
You can also name cascading values, so try this: first name the Increment cascading parameter as shown below, then update the test as shown below.
[CascadingParameter(Name = "Increment")]
public int Increment { get; set; }
var cut = RenderComponent<CounterWithCV>(parameters =>
parameters.AddCascadingValue("Increment", 3));
Creating Mock Implementations with Moq
We've seen that components should do one thing well (Single Responsibility Principle), and we should use services to implement logic, such as retrieving data using REST or implementing business logic. We use dependency injection to pass these services to components. Here, we'll learn how to use bUnit to pass dependencies to components and how to replace your services with mock implementations to better drive your unit tests.
Using bUnit for Dependency Injection
Let's start with the FetchData component below. This component has a dependency, which is IWeatherService.
@page "/fetchdata"
@using Testing.Shared
@inject IWeatherService WeatherService
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private IEnumerable<WeatherForecast>? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await WeatherService.GetForecasts();
}
}
When using this component in a Blazor application, the Blazor runtime is responsible for injecting this dependency. When you use the component in a bUnit test, the bUnit runtime is responsible for injecting the dependency. The only thing we need to specify is which class to use to instantiate the instance.
Add a new test class to the test project, name it FetchDataShould, and complete it as shown below. To configure dependency injection in a bUnit test, use the same methods as with regular dependency injection—AddSingleton, AddTransient, and AddScoped—to add dependencies to the Services property.
using Bunit;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Testing.Client.Pages;
using Testing.Shared;
using Xunit;
namespace Testing.ComponentTests
{
public class FetchDataShould : TestContext
{
[Fact]
public void UseWeatherService()
{
// Use Services for dependency injection
Services.AddSingleton<IWeatherService, Testing.Shared.WeatherService>();
var cut = RenderComponent<FetchData>();
var rows = cut.FindAll("tbody tr");
rows.Count.Should().Be(5);
}
}
}
Try running this test. It fails? Look at the output of the failing test. It turns out that the WeatherService in the Shared project has its own dependency, which is ILogger. Should we add another dependency? In this case, we should build a class that implements the ILogger interface or find an existing one. We won't. Let's talk about mock objects.
Replacing Dependencies with Mock Objects
When testing components, you want to have full control over dependencies. This means in many cases, you can't use the real dependencies. First, remember that tests should be fast and automatic? If the real dependencies use a database or REST calls to get data, this will make your tests slow. Network, disk, and database are several factors that are slower than accessing data from memory. So we want to avoid these things. Also, databases and disks have persistence, so when tests modify data, the next time the test runs, it will use different data and might fail. So we don't want to use real dependencies (we're testing the component, not the dependencies!). Therefore, we'll use mock implementations of dependencies, which is why it's so important to make your dependencies implement interfaces. Building another class with the same interface is both simple and practical.
And there are different kinds of mock objects. Let's discuss stubs and mocks as shown below. As you can see, stubs and mocks are both special cases of fakes. Unfortunately, the terminology used here (stub, mock, fake) is not consistent in the testing community. Some people use different names to categorize fake objects, and some even use a taxonomy that includes seven different stubs!
Using Stubs
Let's start with stubs. A stub is a fake implementation of a dependency, used only to assist in testing. Our FetchData component will get a few forecasts from the IWeatherService dependency. But how many forecasts will this return? If we use the real service, this might depend on a bunch of things we can't control. So we use a stub implementation of IWeatherService that we have full control over. A stub is just used to assist in testing, and we'll perform the assert phase on the System Under Test, not on the stub.
Let's build a stub for IWeatherService. First, add a new class named WeatherServiceStub to the test project. Implement the interface as shown below. Our stub has a property that will hold the data that will be returned from the service.
private class WeatherServiceStub : IWeatherService
{
public IEnumerable<WeatherForecast> FakeForecasts { get; set; }
= default!;
public ValueTask<IEnumerable<WeatherForecast>> GetForecasts()
=> new ValueTask<IEnumerable<WeatherForecast>>(
FakeForecasts);
}
Now update the UseWeatherService test as shown below. We create an instance of the stub, initialize it with the data we want, and then pass it as a singleton to the dependency injection. When the FetchData component is initialized, it will use the stub, and we're sure that our service will return five rows of data (or a different number; that's why I use const for easy updating).
[Fact]
public void UseWeatherService()
{
const int nrOfForecasts = 5;
var stub = new WeatherServiceStub
{
FakeForecasts = Enumerable.Repeat(new WeatherForecast(), nrOfForecasts)
};
Services.AddSingleton<IWeatherService>(stub);
var cut = RenderComponent<FetchData>();
var rows = cut.FindAll("tbody tr");
rows.Count.Should().Be(nrOfForecasts);
}
Using Mocks
So what's a mock? A mock is a fake implementation where we want to verify that the System Under Test calls certain methods and properties on the mock. So a mock is a bit like a data recorder, remembering which methods were called and even recording the values of parameters in method calls. Building a mock takes more work, which is no surprise! When you use a mock in a test, you'll do the assert phase through the mock, asking questions like "Did the System Under Test call this method?".
Let's update the FetchData component to do some logging, so add an @inject for ILogger and use it in OnInitializedAsync as shown below.
@page "/fetchdata"
@using Microsoft.Extensions.Logging
@using Testing.Shared
@inject IWeatherService WeatherService
@inject ILogger logger
...
protected override async Task OnInitializedAsync()
{
logger.LogInformation("Fetching forecasts");
forecasts = await WeatherService.GetForecasts();
}
So we want to test if ILogger is used during OnInitializedAsync. We need a mock implementation because we don't want to parse log files. Add a new class named LoggerMock to your test project as shown below. Implementing this class by itself takes some work! Next, we'll look at how to make this easier. Our mock logger just logs a few parameters in a list.
private class LoggerMock : ILogger
{
public List<(LogLevel logLevel, object? state)> Journal
{ get; set; } = new List<(LogLevel, object?)>();
public IDisposable BeginScope<TState>(TState state)
=> throw new NotImplementedException();
public bool IsEnabled(LogLevel logLevel)
=> true;
public void Log<TState>(LogLevel logLevel, EventId eventId,
TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
Journal.Add((logLevel, state));
}
}
Add a new unit test to the FetchDataShould class as shown below.
[Fact]
public void UseProperLogging()
{
const int nrOfForecasts = 5;
var stub = new WeatherServiceStub
{
FakeForecasts = Enumerable.Repeat(new WeatherForecast(), nrOfForecasts)
};
Services.AddSingleton<IWeatherService>(stub);
LoggerMock logger = new LoggerMock();
Services.AddSingleton<ILogger>(logger);
var cut = RenderComponent<FetchData>();
logger.Journal.Count.Should().Be(1);
logger.Journal.First().state.Should().NotBeNull();
logger.Journal.First().state!.ToString().Should().Contain("Fetching forecasts");
}
Implementing Stubs and Mocks with Moq
How can we implement stubs and mocks with less work? Other people have been asking the same question, and some have built libraries that make this possible. These libraries are often called isolation frameworks. Isolation frameworks allow you to quickly generate stubs and mocks for classes and interfaces, where you only implement the methods needed for the test, and verify that the System Under Test calls a method with certain parameters a certain number of times. Here, we'll look at Moq, one of the most popular isolation frameworks in the testing community today. We'll cover many features of Moq here, but if you want to learn more, you can visit https://documentation.help/Moq.
First, add the Moq NuGet package to your test project. Now copy the UseWeatherServices method and rename it to UseWeatherServicesMoq. Change its implementation as shown below. First, we create the forecast data we want IWeatherService to return. Next, we create an instance of Mock<IWeatherService>, which is a class in Moq. This class allows us to set up methods on an interface and return a certain result. Providing a stub implementation is that simple. But Moq allows you to go further and make methods return different results, for example, depending on parameters.
Next, we configure bUnit's dependency injection to inject a singleton instance, passing stub.Object, which is an instance that implements the IWeatherService interface. No need to build our own class to create a stub.
Our FetchData component also needs a logger, but here we're not interested in the interaction between the component and the logger, so we create another stub. The rest of the test stays the same.
[Fact]
public void UseWeatherServiceMoq()
{
const int nrOfForecasts = 5;
var forecasts = Enumerable.Repeat(new WeatherForecast(), nrOfForecasts);
Mock<IWeatherService> stub = new Mock<IWeatherService>();
stub.Setup(s => s.GetForecasts())
.Returns(new ValueTask<IEnumerable<WeatherForecast>>(forecasts));
Services.AddSingleton<IWeatherService>(stub.Object);
Mock<ILogger> loggerStub = new Mock<ILogger>();
Services.AddSingleton<ILogger>(loggerStub.Object);
var cut = RenderComponent<FetchData>();
var rows = cut.FindAll("tbody tr");
rows.Count.Should().Be(nrOfForecasts);
}
Run the test; it should pass. Now it's time to implement a mock, and we want to see if the FetchData component calls the logger. Copy the UseProperLogging method and name it UseProperLoggingMoq as shown below. Here, you should focus on the Verify method. Here, we verify that the Log method was called, and we can specify how many times. You can choose between "Never", "Once", "AtLeast", "AtMost", "Exactly", etc. The Log method takes a bunch of parameters, and this Log method works in a somewhat awkward way. The first parameter is of type LogLevel, and we check if the LogLevel.Information value matches
It.Is<LogLevel>(l => l == LogLevel.Information)
Each parameter is represented with a check of the parameter's value. You can also use It.IsAny<T> to ignore the parameter's value, specifying the type of the parameter. This type of parameter is needed to disambiguate overloads. Other parameters work similarly, even for generic parameters. For example, if a parameter's type is List<T>, and you don't know T, use It.Is<List<It.IsAnyType>>. Due to the specific implementation details of ILogger, we need to use it here.
[Fact]
public void UseProperLoggingMoq()
{
const int nrOfForecasts = 5;
var forecasts = Enumerable.Repeat(new WeatherForecast(), nrOfForecasts);
Mock<IWeatherService> stub = new Mock<IWeatherService>();
stub.Setup(s => s.GetForecasts())
.Returns(new ValueTask<IEnumerable<WeatherForecast>>(forecasts));
Services.AddSingleton<IWeatherService>(stub.Object);
Mock<ILogger> loggerMock = new Mock<ILogger>();
Services.AddSingleton<ILogger>(loggerMock.Object);
var cut = RenderComponent<FetchData>();
loggerMock.Verify(
l => l.Log(
It.Is<LogLevel>(l => l == LogLevel.Information),
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(msg, t) => msg.ToString()!
.Contains("Fetching forecasts")),
It.IsAny<Exception>(),
It.Is<Func<It.IsAnyType, Exception?, string>>(
(v, t) => true))
, Times.Once);
}
Run the test. It should pass.
Writing Unit Tests in Razor
When you build unit tests with bUnit, sometimes they become long because of all the generated markup. Also, the MarkupMatches method takes a string, and if your markup uses HTML attributes, you need to escape quotes with \\. For these kinds of tests, we can also write tests in Razor. Writing unit tests in Razor requires two things: the project needs to reference the Razor SDK, which means your test project should set the SDK type to Razor:
<Project Sdk="Microsoft.NET.Sdk.Razor">
Second, you should add an _Imports.razor file in the test project for easier referencing as shown below.
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using Microsoft.Extensions.DependencyInjection
@using AngleSharp.Dom
@using Bunit
@using Bunit.TestDoubles
@using Xunit
I also recommend adding your project's namespaces here.
First Razor Test
In your test project, add a new razor component named RCounterShould as shown below. Here, I'll add an R prefix to razor unit tests so we don't get name conflicts with other CounterShould test classes. We'll make the test inherit from TestContext, just like our C# test classes. Then we add an @code section and put our xUnit test methods there. Because this is a razor file, we can use razor to write the test's markup in the Render method.
In the MarkupMatches method, we can also use plain razor to write markup. This makes writing such tests simpler and more enjoyable.
@inherits Bunit.TestContext
@code {
[Fact]
public void RenderCorrectlyWithInitialZero()
{
var cut = Render(@<Counter />);
cut.Find("p")
.MarkupMatches(@<p>Current count: 0</p>);
}
[Fact]
public void IncrementCounterWhenButtonIsClicked()
{
var cut = RenderComponent<Counter>();
cut.Find(cssSelector: "button")
.Click();
cut.Find(cssSelector: "p")
.MarkupMatches(@"<p>Current count: 1</p>");
}
}
Passing Parameters
What about passing parameters? Add a new component named RTwoWayCounterShould as shown below. Since we can render our components using plain razor, we can pass parameters in razor syntax as in the first test method! The second test method illustrates how we can test two-way data binding, again using the familiar razor syntax.
@inherits Bunit.TestContext
@code {
[Fact]
public void IncrementCounterWhenButtonIsClicked()
{
var cut = Render(@<TwoWayCounter CurrentCount="1" Increment="2"/>);
cut.Find("button").Click();
cut.Find("p")
.MarkupMatches(@<p>Current count: 3</p>);
}
[Fact]
public void TriggerChangedEventForCurrentCounter2()
{
int currentCount = 1;
var cut = Render(@<TwoWayCounter
@bind-CurrentCount="currentCount"
Increment="2"/>);
cut.Find(cssSelector: "button")
.Click();
currentCount.Should().Be(3);
}
}
Let's look at an example using ChildContent. Add a new razor component called RAlertShould to the test project as shown below. The Alert component uses ChildContent, and we can pass it by nesting the child content inside the Alert tags. To see if the component renders as expected, we can use simple HTML markup in the MarkupMatches method.
@inherits Bunit.TestContext
@code {
[Fact]
public void RenderSimpleChildContent()
{
var cut = Render(
@<Alert>
<h1>Hello world!</h1>
</Alert>
);
cut.MarkupMatches(
@<div class="alert alert-secondary mt-4" role="alert">
<h1>Hello world!</h1>
</div>
);
}
}
Add another razor component called RTemplatedListShould as shown below. Again, we want to see if the component shows the loading RenderFragment when items are null. Again, use razor to pass the RenderFragment.
@inherits Bunit.TestContext
@code {
[Fact]
public void RenderLoadingTemplateWhenItemsIsNull()
{
RenderFragment loading =
@<div class="loader">Loading...</div>;
Func<ValueTask<IEnumerable<string>?>> loader =
() => new ValueTask<IEnumerable<string>?>(
result: null);
var cut = Render(
@<TemplatedList Loader="@loader">
<LoadingContent>
<div class="loader">Loading...</div>
</LoadingContent>
</TemplatedList>
);
cut.Find("div.loader")
.MarkupMatches(loading);
}
}
Handling Asynchronous Re-rendering
When you build components that override OnInitializedAsync or OnParametersSetAsync, your component will render itself at least twice—first when the component is created and after OnInitializedAsync completes, and again after each OnParametersSetAsync completes.
In bUnit tests, this can give you problems. Let's look at an example.
Add the following unit test to the RTemplatedListShould class as shown below. In this test, we use the TaskCompletionSource<T> class to make the loader truly asynchronous. An instance of this class has a Task<T> that will be continued by calling the SetResult method. Until then, the task will block any awaiter. This allows us to render the component, see the loading UI, and then complete the task by calling SetResult, and then see if the items are rendered.
[Fact]
public void RenderItemsAfterItemsLoadedAsyncCorrectly()
{
const int count = 5;
var tcs = new TaskCompletionSource<IEnumerable<string>?>();
Func<ValueTask<IEnumerable<string>?>> loader =
() => new ValueTask<IEnumerable<string>?>(tcs.Task);
var cut = Render(
@<TemplatedList Loader="@loader">
<LoadingContent>
<div class="loader">Loading...</div>
</LoadingContent>
<ItemContent Context="item">
<ListItem Item="@item" />
</ItemContent>
</TemplatedList>
);
cut.Find("div.loader")
.MarkupMatches(@<div class="loader">Loading...</div>);
// Complete the loader task,
// this should rerender the component asynchronously
tcs.SetResult(Enumerable.Repeat("A", count));
var ps = cut.FindAll("p");
ps.Should().NotBeEmpty();
foreach (var p in ps)
{
p.MarkupMatches(@<p>A</p>);
}
}
Run the test. It will fail! Why? Because our component will render the UI on another thread, and the test will check the UI before the rendering is complete. So what should we do? Add this line after the SetResult call, using the complete method below.
cut.WaitForState(() => cut.FindAll("p").Any());
The WaitForState method will wait until the condition returns true. We know that the UI will render a bunch of paragraphs, so we wait until we see them. WaitForState also has a parameter (not shown here) to set a timeout, which defaults to 1 second. If the cut doesn't pass the condition within the timeout, the test will fail with a WaitForFailedException.
[Fact]
public void RenderItemsAfterItemsLoadedAsyncCorrectly()
{
const int count = 5;
var tcs = new TaskCompletionSource<IEnumerable<string>?>();
Func<ValueTask<IEnumerable<string>?>> loader =
() => new ValueTask<IEnumerable<string>?>(tcs.Task);
var cut = Render(
@<TemplatedList Loader="@loader">
<LoadingContent>
<div class="loader">Loading...</div>
</LoadingContent>
<ItemContent Context="item">
<ListItem Item="@item" />
</ItemContent>
</TemplatedList>
);
cut.Find("div.loader")
.MarkupMatches(@<div class="loader">Loading...</div>);
// Complete the loader task,
// this should rerender the component asynchronously
tcs.SetResult(Enumerable.Repeat("A", count));
// Wait for rendering to complete
cut.WaitForState(() => cut.FindAll("p").Any());
var ps = cut.FindAll("p");
ps.Should().NotBeEmpty();
foreach (var p in ps)
{
p.MarkupMatches(@<p>A</p>);
}
}
Configuring Semantic Comparison
The bUnit testing library uses the AngleSharp Diffing library to compare the generated markup with the expected markup in the MarkupMatches method. You can find AngleSharp on GitHub at https://github.com/AngleSharp/AngleSharp.Diffing. To make your tests more robust, you can configure how the semantic comparison works; for example, we can tell it to ignore certain HTML attributes and elements.
Why Do We Need Semantic Comparison?
Using strings to compare markup is too sensitive to minor changes in the markup. For example, formatting your code might add some whitespace, and since string comparison compares each character, a working test would suddenly fail. And there are many innocent changes that would break tests, such as changing the order of attributes, or reordering classes in the class attribute, or adding comments. Semantic comparison will ignore all these changes, causing tests not to break because of a simple change.
Customizing Semantic Comparison
Remember one of our earlier tests where we told the MarkupMatches method to ignore the style attribute (example below). The AngleSharp Diffing library allows us to use special attributes to ignore certain elements and attributes; for example, <div style:ignore> will ignore the content of the style attribute. We can also tell it to ignore certain HTML elements; for example, add the test below to the AlertShould class.
[Fact]
public void RenderCorrectly()
{
var cut = RenderComponent<Alert>(parameters =>
parameters.AddChildContent("<p>Hello world!</p>"));
cut.MarkupMatches(@"
<div class=""alert alert-secondary mt-4"" role=""alert"">
<p diff:ignore></p>
</div>");
}
We can do the same for razor tests, for example, the test below, which should be added to the RAlertShould razor file.
[Fact]
public void RenderCorrectly()
{
var cut = Render(
@<Alert>
<h1>Hello world!</h1>
</Alert>
);
cut.MarkupMatches(
@<div class="alert alert-secondary mt-4" role="alert">
<h1 diff:ignore></h1>
</div>
);
}
By default, semantic comparison ignores whitespace, but in some cases, you want to verify that the component actually renders some whitespace. Use diff:whitespace="preserve" for this.
You can also tell semantic comparison to ignore case or use regular expressions for comparison.
Let's test the simple Card component below.
<h3 id="card-@Id">Card @Id</h3>
@code {
[Parameter]
public int Id { get; set; }
}
A unit test will check if the id attribute matches card- followed by one to four digits and if the content matches Card with one to four digits, as shown below. We also want the test to ignore the case of the card content.
using Bunit;
using Testing.Client.Pages;
using Xunit;
namespace Testing.ComponentTests
{
public class CardShould : TestContext
{
[Fact]
public void RenderCorrectlyWithProperId()
{
var cut = RenderComponent<Card>();
cut.MarkupMatches(@"<h3 diff:ignorecase diff:regex id:regex=""card-\d{1,4}">card \d{1,4}</h3>");
}
}
}