Unit Testing Blazor Components with bUnit and JustMock_1200x303

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 the Blazor App Project

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.

Creating the Unit Test Project

Setting up the unit test project is not as straightforward as the Blazor application. Just follow the steps below and you should be fine.

  1. As a first step create a Razor Class Library targeting .NET Core 3.1. The same version should be for the Blazor application as well.
  2. Add the Microsoft.NET.Test.Sdk NuGet package to the project.
  3. Add bUnit as a NuGet package to the project. Please notice that this package is still in beta and you will need to check the include prerelease checkbox. When trying to add the package you will notice the following error:

Error bUnit NuGet installation

This is because the Razor Class Library project is targeting .NET standard. Change it to target .NET core app 3.1. The result in the project file should be the following:

<PropertyGroup>
  <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

And again, try to add the bUnit NuGet package.

  1. Add xUnit as a NuGet package to the project. I am using xUnit as I saw that it is supported by bUnit.
  2. Add the xunit.runner.visualstudio NuGet package to the project. We need this package to execute the unit tests in Visual Studio.
  3. Add the JustMock NuGet package (this is the free version of JustMock - for historical reasons it is not renamed to JustMock Lite).
  4. Add a reference to the Blazor Demo application.
  5. Build to validate there are no errors.

OK, we have created the test project and we successfully built it. Now we need to prepare out test class.

  1. Add one class that will be used for the unit tests
  2. Add usings to Bunit, Xunit and Telerik.JutMock
  3. Make the class public
  4. Inherit the ComponentTestFixture abstract class

Now we are ready to start writing our first test.

The Simplest Unit 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 cut = 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>");
}

Testing a Component Which Uses Data

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.

Forecast in Null Scenario

The first step for testing this scenario is registering the WeatherForecastService.Services.AddSingleton();

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 cut = 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 weatherForecastServiceMock = Mock.Create<IWeatherForecastService>();
    Mock.Arrange(() => weatherForecastServiceMock.GetForecastAsync(Arg.IsAny<DateTime>()))
        .Returns(new TaskCompletionSource<WeatherForecast[]>().Task);
    Services.AddSingleton<IWeatherForecastService>(weatherForecastServiceMock);
 
    // Act
    var cut = 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);
}

Forecast Has a Value

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));
 
    Services.AddSingleton<IWeatherForecastService>(weatherForecastServiceMock);
 
    // Act - render the FetchData component
    var cut = RenderComponent<FetchData>();
    var actualForcastDataTable = cut.FindComponent<ForecastDataTable>(); // find the component
 
    // Assert
    var expectedDataTable = RenderComponent<ForecastDataTable>((nameof(ForecastDataTable.Forecasts), forecasts));
    actualForcastDataTable.MarkupMatches(expectedDataTable.Markup);
}

Master-Detail Scenario

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.

Creating the Page

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:

Master Detail Page

Writing the Unit Test for the Master Detail Scenario

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 = 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 =
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));
 
    Services.AddMockJsRuntime();
    Services.AddSingleton<IWeatherForecastService>(weatherForecastServiceMock);
    Services.AddTelerikBlazor();
 
    var rootComponentMock = Mock.Create<TelerikRootComponent>();
 
    var cut = RenderComponent<MasterDetail>(
        CascadingValue(rootComponentMock)
    );
             
    // Act
    IElement plusSymbol = cut.Find("tr.k-master-row td[data-col-index=\"0\"]");
    plusSymbol.Click();
 
    // Assert
    var expectedForecastDetail = 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
About the Author

Mihail Vladov

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.

Related Posts

Comments

Comments are disabled in preview mode.