In this guide to unit testing Blazor components, we'll cover everything from setting up your projects to simple unit test examples and a master-detail scenario.
Blazor is a hot topic these days, and this is why I decided to experiment on what is possible at this early stage in regard to unit testing. To my surprise there is already available library named bUnit for rendering the components under test. And JustMock Lite along with JustMock were perfectly capable of mocking everything I tried.
In this post I will show you some simple examples that may be of interest to you. First, I will start with how to set up your projects, continue with simple unit test examples and finish with a master-detail scenario.
Let's begin setting up your projects. As with any other technology you will need at least two projects. The first one is the Blazor application and the second one is for the unit tests.
Creating your Blazor project should not be a problem as there is a template in Visual Studio 2019.
The newly created project contains default pages and data classes that we will slightly extend and use for the examples.
Setting up the unit test project is not as straightforward as the Blazor application. Just follow the steps below and you should be fine.
<
PropertyGroup
>
<
TargetFramework
>netcoreapp3.1</
TargetFramework
>
</
PropertyGroup
>
OK, we have created the test project and we successfully built it. Now we need to prepare out test class.
Now we are ready to start writing our first test.
If you start the Blazor Demo application that we have added, you will notice that in the left navigation there are three links. Home, Counter and Fetch Data. I will test the functionality in the Counter page where clicking a button will increase the counter.
To do this I will have to first render the Counter page. Find where the counter value is located. Validate that this is zero. Click on the button and validate that the counter value has changed to 1. Here is what this test will look like:
[Fact]
public
void
TestCounter()
{
// Arrange
var ctx =
new
TestContext();
var cut = ctx.RenderComponent<Counter>();
cut.Find(
"p"
).MarkupMatches(
"<p>Current count: 0</p>"
);
// Act
var element = cut.Find(
"button"
);
element.Click();
//Assert
cut.Find(
"p"
).MarkupMatches(
"<p>Current count: 1</p>"
);
}
In the default Blazor application there is a page named FetchData which uses the WeatherForecastService to fetch the data and show it. I want to show how useful it is to use bUnit to obtain certain components and this is why I will move the part with representing the weather forecast data in a separate component. Here is how the code looks:
@page
"/fetchdata"
@
using
BlazorDemoApp.Data
@
using
BlazorDemoApp.Components
@inject WeatherForecastService ForecastService
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
@
if
(forecasts ==
null
)
{
<p><em>Loading...</em></p>
}
else
{
<ForecastDataTable Forecasts=
"forecasts"
/>
}
@code {
private
WeatherForecast[] forecasts;
protected
override
async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}
And here is how the ForecastDataTable component looks like:
<table
class
=
"forcast-data-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
WeatherForecast[] _forecasts = Array.Empty<WeatherForecast>();
[Parameter]
public
WeatherForecast[] Forecasts
{
get
=> _forecasts;
set
=> _forecasts = value ?? Array.Empty<WeatherForecast>();
}
}
Now I can start writing the tests. For this particular page I need to write two unit tests. The first should test that the loading text is shown when the forecasts variable is null. And the second one is to test that the data is shown correctly when there is actual data.
The first step for testing this scenario is registering the WeatherForecastService.Services.AddSingleton<WeatherForecastService>();
After that I will need to render the FetchData page and validate that the loading text is shown. Here is how the unit test looks like:
[Fact]
public
void
TestFetchData_NullForecast()
{
Services.AddSingleton<WeatherForecastService>();
var ctx =
new
TestContext();
var cut = ctx.RenderComponent<FetchData>();
// Assert that it renders the initial loading message
var initialExpectedHtml =
@"<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
<p><em>Loading...</em></p>";
cut.MarkupMatches(initialExpectedHtml);
}
If you execute this test now you will notice that it will fail. The reason for this is because the WeatherForecastService generates random values and the forecasts variable in the component will never be null.
This is a good candidate for mocking. If you are not familiar with the concept, in mocking, to test the required logic in isolation the external dependencies are replaced by closely controlled replacements objects that simulate the behavior of the real ones. For our scenario the external dependency is the call to the WeatherForecastService.GetForecastAsync method.
To mock this method, I will have to use a mocking framework like Telerik JustMock. It is capable of mocking literally everything - from public to internal, private or static API, even members of MsCorLib like DateTime.Now and more.
There is a free version of JustMock named JustMock Lite. JustMock Lite has the limitation to be able to mock only public virtual methods and public interfaces. You could check this comparison table between JustMock and JustMock Lite to see the difference.
Back to the code. For this blog post I decided to use JustMock Lite and because of this I will need to modify the WeatherForecastService to inherit an interface and work with that interface instead. Here is what this will look like:
public
interface
IWeatherForecastService
{
Task<WeatherForecast[]> GetForecastAsync(DateTime startDate);
}
To work with this interface instead of the actual implementation several changes should be made. First the WeatherForecastService should inherit this interface.
Next, the FetchData page should use it. Here is the chunk of code that should be modified:
@page
"/fetchdata"
@
using
BlazorDemoApp.Data
@
using
BlazorDemoApp.Components
@inject IWeatherForecastService ForecastService
Also, for the BlazorDemo App to continue working I need to modify the Startup.ConfigureServices method and add IWeatherForecastService as a singleton service with implementation WeatherForecastService. Like this:
public
void
ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<IWeatherForecastService, WeatherForecastService>();
}
Now we are ready to create the mock.
What I will do is create a mock of type IWeatherForecastService and arrange the GetForecastAsync method for any DateTime argument to return a value that will result in null value for the forecast variable. At the end the mocked instance should be registered as implementation of our interface. Here is how the whole test looks:
[Fact]
public
void
TestFetchData_ForecastIsNull()
{
// Arrange
var ctx =
new
TestContext();
var weatherForecastServiceMock = Mock.Create<IWeatherForecastService>();
Mock.Arrange(() => weatherForecastServiceMock.GetForecastAsync(Arg.IsAny<DateTime>()))
.Returns(
new
TaskCompletionSource<WeatherForecast[]>().Task);
ctx.Services.AddSingleton<IWeatherForecastService>(weatherForecastServiceMock);
// Act
var cut = ctx.RenderComponent<FetchData>();
// Assert - that it renders the initial loading message
var initialExpectedHtml =
@"<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
<p><em>Loading...</em></p>";
cut.MarkupMatches(initialExpectedHtml);
}
For this scenario what I will do is create a mock of the GetForecastAsync similarly to what I did in the previous test, but this time the method will return a single predefined value. I will use this value later for validation.
Next I will register the IWeatherForecastService with the implementation of the created mock. After that I will render the FetchData component. bUnit has an API that allows me to search for a nested component in another component. This is what I will do as I have already extracted the forecast data representation in another component. At the end I will compare the actual result with expected value. Here is what this unit test will look like:
[Fact]
public
void
TestFetchData_PredefinedForecast()
{
// Arrange
var forecasts =
new
[] {
new
WeatherForecast { Date = DateTime.Now, Summary =
"Testy"
, TemperatureC = 42 } };
var weatherForecastServiceMock = Mock.Create<IWeatherForecastService>();
Mock.Arrange(() => weatherForecastServiceMock.GetForecastAsync(Arg.IsAny<DateTime>()))
.Returns(Task.FromResult<WeatherForecast[]>(forecasts));
var ctx =
new
TestContext();
ctx.Services.AddSingleton<IWeatherForecastService>(weatherForecastServiceMock);
// Act - render the FetchData component
var cut = ctx.RenderComponent<FetchData>();
var actualForcastDataTable = cut.FindComponent<ForecastDataTable>();
// find the component
// Assert
var expectedDataTable = ctx.RenderComponent<ForecastDataTable>((nameof(ForecastDataTable.Forecasts), forecasts));
actualForcastDataTable.MarkupMatches(expectedDataTable.Markup);
}
The last scenario that I wanted to show is how to test a master-detail. To save time in development of this case I will use the Teleik Blazor Grid, as it has built-in master-detail support. To be able to run the scenario you should download the Telerik Blazor trial if you don’t already have a license. For more information you could read the What You Need to Use the Telerik Blazor Components documentation article.
As a first step I must create a new page. I will use a MasterDetail name for this. Here is how the code of this page looks:
@page
"/master-detail"
@
using
BlazorDemoApp.Data
@
using
BlazorDemoApp.Components
@inject IWeatherForecastService ForecastService
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
@
if
(forecasts ==
null
)
{
<p><em>Loading...</em></p>
}
else
{
<ForecastDataGrid Forecasts=
"forecasts"
/>
}
@code {
private
WeatherForecast[] forecasts;
protected
override
async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}
As you can see it is the same as the previous one, with the only difference that it is using the ForecastDataGrid component. And here is what the code of the ForecastDataGrid component looks like:
<
div
class
=
"forecast-with-telerik-grid"
>
<
TelerikGrid
Class
=
"forecast-grid"
Data
=
"@Forecasts"
Pageable
=
"true"
PageSize
=
"10"
Sortable
=
"true"
Height
=
"500px"
Reorderable
=
"true"
Resizable
=
"true"
Groupable
=
"true"
FilterMode
=
"GridFilterMode.FilterMenu"
>
<
GridColumns
>
<
GridColumn
Field
=
"@(nameof(WeatherForecast.Date))"
Title
=
"Date"
Width
=
"100px"
Groupable
=
"false"
/>
<
GridColumn
Field
=
"@(nameof(WeatherForecast.TemperatureC))"
Title
=
"Temp. (C)"
Width
=
"80px"
/>
<
GridColumn
Field
=
"@(nameof(WeatherForecast.TemperatureF))"
Title
=
"Temp. (F)"
Width
=
"80px"
/>
<
GridColumn
Field
=
"@(nameof(WeatherForecast.Summary))"
Title
=
"Summary"
Width
=
"120px"
/>
</
GridColumns
>
<
DetailTemplate
>
@{
WeatherForecast weatherForecast = context as WeatherForecast;
<
WeatherForecastDetail
WeatherForecast
=
"@weatherForecast"
></
WeatherForecastDetail
>
}
</
DetailTemplate
>
</
TelerikGrid
>
</
div
>
@code {
private WeatherForecast[] _forecasts = Array.Empty<
WeatherForecast
>();
[Parameter]
public WeatherForecast[] Forecasts
{
get => _forecasts;
set => _forecasts = value ?? Array.Empty<
WeatherForecast
>();
}
}
And here is what the code of the WeatherForecastDetail component looks like:
<div
class
=
"weather-forecast-detail"
>
@
if
(WeatherForecast !=
null
)
{
<div
class
=
"row my-4"
>
<div
class
=
"col-sm-12"
>
<h3
class
=
"h1"
>
@WeatherForecast.Date.ToString(
"dd MMMM yyyy"
,
new
CultureInfo(
"en-US"
))
</h3>
</div>
</div>
<div
class
=
"row my-4"
>
<div
class
=
"col-sm-2"
>
<span
class
=
"small d-block text-muted"
>Temperature
in
Celsius</span>
@WeatherForecast.TemperatureC °
</div>
<div
class
=
"col-sm-2"
>
<span
class
=
"small d-block text-muted"
>Temperature
in
Fahrenheit</span>
@WeatherForecast.TemperatureF °
</div>
<div
class
=
"col-sm-2"
>
<span
class
=
"small d-block text-muted"
>Summary</span>
@WeatherForecast.Summary
</div>
</div>
}
else
{
<div
class
=
"alert alert-primary"
role=
"alert"
>
Please select a weather forecast to see its details.
</div>
}
</div>
@code {
[Parameter]
public
WeatherForecast WeatherForecast {
get
;
set
; }
}
If you paste this code and build it, you will notice an error stating that the Telerik Grid can’t be found. To solve this add the Telerik.Blazor and Telerik.Blazor.Components usings to the _Imports.razor file.
@
using
Telerik.Blazor
@
using
Telerik.Blazor.Components
As a next step I must add the TelerikRootComponent in the MainLayout page. This is required to allow the Telerik Blazor Grid to work with detached popups as it was explained in the documentation article I've provided above. Here is how the MainLayout should look:
@inherits LayoutComponentBase
<
TelerikRootComponent
>
<
div
class
=
"sidebar"
>
<
NavMenu
/>
</
div
>
<
div
class
=
"main"
>
<
div
class
=
"top-row px-4"
>
<
a
href
=
"https://docs.microsoft.com/aspnet/"
target
=
"_blank"
>About</
a
>
</
div
>
<
div
class
=
"content px-4"
>
@Body
</
div
>
</
div
>
</
TelerikRootComponent
>
I have created the master detail page, and the build is passing. Now I need to add the new page to the navigation. To do so, I need to modify the NavMenu page and add the following:
<
li
class
=
"nav-item px-3"
>
<
NavLink
class
=
"nav-link"
href
=
"master-detail"
>
<
span
class
=
"oi oi-list-rich"
aria-hidden
=
"true"
></
span
> Master Detail
</
NavLink
>
</
li
>
Another thing that needs to be done is the registration of the Telerik Blazor service in the Startup.ConfigureServices method. Here is how the new code will look like:
public
void
ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<IWeatherForecastService, WeatherForecastService>();
services.AddTelerikBlazor();
}
And lastly I need to add the JavaScript and CSS that are used by the Telerik Blazor components in the _Host.cshtml file. Here are the lines that needs to be added:
<
link
rel
=
"stylesheet"
href
=
"_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css"
/>
<
script
src
=
"_content/telerik.ui.for.blazor/js/telerik-blazor.js"
defer></
script
>
The result is that in my BlazorDemo application I have a master detail page. Here is an image of the final result:
What I want to verify is that the master detail is working as expected. For that purpose I want to make the unit test render the page, click on the plus sign and verify that the detail component is rendered with the correct data.
To achieve this I will again create a mock of IWeatherForecastService and arrange it to return a predefined single value which will be used later for comparison. For this case a mock of JsRuntime should be registered as a service. bUnit provides such mock out of the box and can be used by simply adding Services.AddMockJsRuntime(); I must register the TeleirkBlazor service as well.
As the Telerik Grid has a requirement for the root element to be TelerikRootComponent, I will create a mock of TelerikRootComponent and render the grid as nested component. Here is how:
var rootComponentMock = Mock.Create<TelerikRootComponent>();
var cut = ctx.RenderComponent<MasterDetail>(
CascadingValue(rootComponentMock)
);
After the rendering is done I will have to find the plus sign and click on it. Here is how:
IElement plusSymbol = cut.Find(
"tr.k-master-row td[data-col-index=\"0\"]"
);
plusSymbol.Click();
Next I will make a separate rendering of the WeatherForecastDetail component with the predefined values. This rendering will be used as the expected value in the comparison. Here is how:
var expectedForecastDetal =
ctx.RenderComponent<WeatherForecastDetail>((nameof(WeatherForecastDetail.WeatherForecast), forecasts[0]));
And lastly, I will have to find the actual detail component and compare it to the expected one. Here is how:
var actualForecastDetailElement = cut.FindComponent<WeatherForecastDetail>();
actualForecastDetailElement.MarkupMatches(expectedForecastDetail);
And here is what the whole test looks like:
[Fact]
public
void
TestMasterDetail_CorrectValues()
{
// Arrange
var forecasts =
new
[] {
new
WeatherForecast { Date = DateTime.Now, Summary =
"Testy"
, TemperatureC = 42 } };
var weatherForecastServiceMock = Mock.Create<IWeatherForecastService>();
Mock.Arrange(() => weatherForecastServiceMock.GetForecastAsync(Arg.IsAny<DateTime>()))
.Returns(Task.FromResult<WeatherForecast[]>(forecasts));
var ctx =
new
TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.Services.AddSingleton<IWeatherForecastService>(weatherForecastServiceMock);
ctx.Services.AddTelerikBlazor();
var rootComponentMock = Mock.Create<TelerikRootComponent>();
var cut = ctx.RenderComponent<MasterDetail>(parameters => parameters
.AddCascadingValue<TelerikRootComponent>(rootComponentMock)
);
// Act
IElement plusSymbol = cut.Find(
"tr.k-master-row td[data-col-index=\"0\"]"
);
plusSymbol.Click();
// Assert
var expectedForecastDetail = ctx.RenderComponent<WeatherForecastDetail>((nameof(WeatherForecastDetail.WeatherForecast), forecasts[0]));
var actualForecastDetailElement = cut.FindComponent<WeatherForecastDetail>();
// find the component
actualForecastDetailElement.MarkupMatches(expectedForecastDetail);
}
You can find the code used for the examples in this github repo.
If you are interested in the topic and you want to see markup tests or more complex scenarios, please comment in the section bellow.
Mihail Vladov is a Software Engineering Manager at Progress. He has more than a decade of experience with software and product development and is passionate about good software design and quality code. Mihail helped develop the WPF controls suite and Document Processing libraries which are used by thousands of developers. Currently, he is leading the JustMock team. In his free time, he loves to travel and taste different foods. You can find Mihail on LinkedIn.