Test-driven development for Blazor web components with bUnit

In the previous article, the Blazor framework was introduced, which can be used in conjunction with C# to create web UIs. In this second article, we will take a look at a testing library that is ranked in the top 10 NuGet downloads for Blazor [1].

bUnit is a library used to create unit tests for Blazor components. Its stable version has been available since 2021 [2] and is recommended in Blazor’s official documentation for component tests [3].

The documentation for bUnit can be found at https://bunit.dev/

Razor syntax for a web UI using C# instead of JavaScript

Blazor apps are built entirely using Razor components with the file extension .razor. These components use Razor syntax to describe websites or parts of it. This syntax is made up of a mix of HTML, special Razor markup and C#. CSS is used for styling.

The typical Blazor demo page with a counter looks like this:

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

Figure 1: Counter.razor page

In this example, HTML and C# are contained in the same file. For more complex components, it would be helpful to separate these by creating a code-behind file. The file for our Counter.razor component is then named Counter.razor.cs. The compiler automatically combines the two files so that IntelliSense can support us during programming.

To keep the snippets of code in this article shorter, the markup and C# have been separated using a code-behind file in all of the examples.

Components in Blazor: Razor components

To encapsulate elements and make them reusable, we execute functionalities in components. These only contain the markup and the code for an individual functionality. Then, only components are added to the website and any parameters and callbacks are linked. Components can be moved to a library and provided as NuGet packages.

The example shows the same counter after it has been encapsulated in a CounterComponent.razor component. The result displayed in the browser is the same.

@page "/counter"
@using BlazorWasmTemplateApp.Components
<PageTitle>Counter</PageTitle>

<h1>CounterComponent</h1>
<CounterComponent/>

Figure 2: Counter.razor page with component

<p id="currentCount" role="status">Current count: @currentCount</p>

<button id="incrementCountButton" class="btn btn-primary" @onclick="IncrementCount">Click me</button>

Figure 3: Markup for CounterComponent.razor counter component

using Microsoft.AspNetCore.Components;

namespace BlazorWasmTemplateApp.Components
{
    public partial class CounterComponent : ComponentBase
    {
        private int currentCount = 0;

        private void IncrementCount()
        {
            currentCount++;
        }
    }
}

Figure 4: Code for CounterComponent.razor.cs counter component

Unit tests for the UI

Usually, an application’s UI is checked using end-to-end tests (E2E). Frameworks such as Selenium and Playwright are used for this purpose in web development. These tests launch the entire application, render it in a browser and then carry out the test steps. In these types of tests, functionality is often verified through all layers of the application. Due to their complexity, E2E tests are often high-maintenance and slow. As a result, it is generally recommended that you only write a few E2E tests and lots of component tests (test pyramid, see [4]).

If you use test-driven development (TDD) principles for UI development or programme a user interface using extensive logic, you will need to carry out a lot of tests in a short period of time. As a consequence, there are disadvantages to exclusively using E2E tests. bUnit tests are the opposite of E2E tests – they give a good overview, are isolated from each other and, above all, are fast. It often only takes a few milliseconds to execute a test that feels the same as a back end unit test.

bUnit is also a good choice if you are building a Razor component library. It is not without reason that the major sponsors of bUnit include companies like Progress Telerik and Syncfusion, which sell Blazor UI component libraries, among other things.

Unit tests for UI that use bUnit are therefore a good addition to E2E testing, as they allow for details in the UI to be tested.

Getting started with bUnit

Preparations

bUnit is not a framework: it is dependent upon the xUnit, NUnit or MSTest test runners. xUnit is used for the examples in this article. As usual, the tests are run via either the IDE or the command line using dotnet test.


Since we want to use bUnit to test Razor components, we need to change the SDK in the project file for our test project to Microsoft.NET.Sdk.Razor. Details on this can be found in the bUnit documentation at [5].

bUnit is compatible with test projects in the .NET Standard 2.1 version and with all versions of .NET Core from 3.1 and up.

Tests can be programmed in pure C# or Razor syntax. For the sake of clarity, only the variant that uses C# code is shown. When using Razor syntax, it is somewhat easier to pass parameters and callbacks as well as to directly compare HTML with an expected markup. However, this advantage of Razor syntax is irrelevant in practice as comparing HTML produces tests that are difficult to maintain.

The first test

What could a test of our CounterComponent look like? We want to check whether our counter shows 0 in its initial state and 1 after clicking.

using Bunit;

namespace Tests
{
    public class CounterComponentTests : TestContext
    {

        [Fact]
        public void CurrentCount_IsZero_ByDefault()
        {
            //Arrange
            var componentUnderTest = RenderComponent<CounterComponent>();

            //Act
            AngleSharp.Dom.IElement paragraph = componentUnderTest.Find("#currentCount");

            //Assert
            Assert.Equal("Current count: 0", paragraph.InnerHtml);
        }

        [Fact]
        public void CurrentCount_Increases_WhenButtonClicked()
        {
            //Arrange
            var componentUnderTest = RenderComponent<CounterComponent>();
            AngleSharp.Dom.IElement button = componentUnderTest.Find("#incrementCountButton");

            //Act
            button.Click();

            //Assert
            AngleSharp.Dom.IElement paragraph = componentUnderTest.Find("#currentCount");
            Assert.Equal("Current count: 1", paragraph.InnerHtml);
        }
    }
}

Figure 5: Simple CounterComponent test:

Using the generic method RenderComponent<TComponent>() with TComponent as the component type to be tested creates a rendering. This rendering of our CounterComponent, showing its attributes, methods and properties, corresponds to how the components will be displayed in a browser.

In order to perform interactions and check the content of the rendering, we can use the Find() method to identify elements. This method expects a string CSS selector to be a parameter. A CSS selector is a filter for the elements in an HTML DOM that can filter by CSS classes, attributes, IDs or types of HTML element. A good reference for CSS selectors can be found at [6].

The ‘#‘ CSS selector filters by element ID. The #currentCount selector therefore filters out the element with id="currentCount", which contains the current value of the counter in our example component.

To determine whether our test has been successful, we compare the HTML content with an expected text. Though these types of test are fragile and high-maintenance, they act as a good demo example.

Testing components with parameters

What do tests for components with parameters look like? Here, we extend our CounterComponent using a parameter that enables us to set the starting value. As we want to develop our component using a test-driven approach, we add a new test first. In this test, we use an overload of the RenderComponent() method to pass parameters to a ParameterBuilder using a lambda expression parameter.

As we are taking a test-driven approach and the StartValue parameter in our CounterComponent is not available yet, our test is not yet compilable.

[Fact]
public void CurrentCount_IsNotZero_WhenStartValueSet()
{
    //Arrange
    int startValue = 42;
    var componentUnderTest = RenderComponent<CounterComponent>(
        parameterBuilder => parameterBuilder.Add(param => param.StartValue, startValue));

    //Act
    AngleSharp.Dom.IElement paragraph = componentUnderTest.Find("#currentCount");

    //Assert
    Assert.Equal("Current count: " + startValue, paragraph.InnerHtml);
}

Figure 6: Test for our component with a parameter

To make the test compilable, we have to extend our CounterComponentcomponent. In the first step, we add the StartValueproperty of the int type and assign it the parameter attribute. Now, our components can be made compilable. Next, we run our new test and can see that it has failed. We are still in the red phase of the red, green, refactor cycle of TDD and we need to adapt the implementation. To do this, we initialise our currentCount variable in the OnParametersSet() method. This method is called by Blazor after the parameters have been passed to a component. Now, we can run the test again and see that it has been successful. We are therefore in the green phase of our TDD cycle. We do not make any changes in the refactoring phase of a TDD cycle

The code-behind file for our component now looks as follows:

using Microsoft.AspNetCore.Components;

namespace BlazorWasmTemplateApp.Components
{
    public partial class CounterComponent : ComponentBase
    {
        [Parameter]
        public int StartValue { get; set; } = 0;

        private int currentCount = 0;

        protected override void OnParametersSet()
        {
            currentCount = StartValue;
        }

        private void IncrementCount()
        {
            currentCount++;
        }
    }
}

Figure 7: Component extended by one parameter

Testing components with events

Another typical element of Razor components is events. Let’s assume that our website would like to be informed if the value on our counter changes. To do so, we expand our tests once more using the following test:

[Fact]
public void OnCountIncremented_WasInvoked_WhenButtonClicked()
{
    //Arrange
    bool isEventInvoked = false;
    var componentUnderTest = RenderComponent<CounterComponent>(
        parameterBuilder => parameterBuilder
.Add(param => param.OnCountIncremented, () => isEventInvoked = true));

    AngleSharp.Dom.IElement button = componentUnderTest.Find("#incrementCountButton");

    //Act
    button.Click();

    //Assert
    Assert.True(isEventInvoked);
}

Figure 8: Test for our component with an event

Our test is not yet compilable here either. As a result, we add to our component. We add the new OnCountIncremented property of the EventCallback type and assign it the parameter attribute as normal. Our test is now compilable, but it still fails. To ensure that our new test is successful in the next step, we invoke the callback in the IncrementCount() method of our component.

using Microsoft.AspNetCore.Components;

namespace BlazorWasmTemplateApp.Components
{
    public partial class CounterComponent : ComponentBase
    {
        [Parameter]
        public int StartValue { get; set; } = 0;

        [Parameter]
        public EventCallback OnCountIncremented { get; set; }

        private int currentCount = 0;

        protected override void OnParametersSet()
        {
            currentCount = StartValue;
        }

        private async Task IncrementCount()
        {
            currentCount++;
            await OnCountIncremented.InvokeAsync();
        }
    }
}

Figure 9: Component extended by an event

Subcomponents

Razor components can contain Razor components within themselves (subcomponents). It is possible to use bUnit to find these subcomponents in DOM, assign parameters to them, re-render them and execute asserts. Whether these tests are a good idea depends on the scenario. bUnit makes it possible to replace components with test doubles in order to avoid dependencies. This can be particularly useful when using libraries from third-party providers.

Dependency injection

In addition to parameters and events, it is common practice with Razor components to inject dependencies by means of dependency injection. The Blazor framework uses the dependency injection in ASP.NET Core. bUnit therefore makes the Services collection available in the TestContext base class, known from ASP.NET Core. Test doubles can be registered there prior to rendering the component. For dependencies such as IJsRuntime, HttpClient, NavigationManager, etc., which are normally automatically assigned to the components by ASP.NET Core, there are instructions for test doubles in the bUnit documentation.

Summary

bUnit also provides a way for Blazor components to be examined in isolation, quickly and without E2E testing. This provides Blazor developers with an important tool for ensuring the serviceability of the Blazor app through testing. The UI can also be developed using a test-driven approach. In this respect, Blazor is on an equal footing with other SPA frameworks.

Sources

[1] nuget.org. [Online]. Available: https://www.nuget.org/packages?q=Tags%3A%22Blazor%22&sortby=totalDownloads-desc. [Zugriff am 22 Juni 2023].

[2] E. Hansen, „bUnit Releases,“ GitHub, [Online]. Available: https://github.com/bUnit-dev/bUnit/releases?page=3. [Zugriff am 22 Juni 2023].

[3] L. Latham, R. Anderson und GitHubPang, „Test Razor components in ASP.NET Core Blazor,“ Microsoft Learn, 04 April 2023. [Online]. Available: https://learn.microsoft.com/en-us/aspnet/core/blazor/test. [Zugriff am 22 Juni 2023].

[4] M. Cohn, Succeeding with Agile: Software Development Using Scrum, Addison-Wesley, 2009.

[5] E. Hansen, „Creating a new bUnit test project,“ bUnit, 7 April 2023. [Online]. Available: https://bunit.dev/docs/getting-started/create-test-project.html. [Zugriff am 22 Juni 2023].

[6] Refsnes Data, „CSS Selector Reference,“ W3Schools, [Online]. Available: https://www.w3schools.com/cssref/css_selectors.php. [Zugriff am 22 Juni 2023].

[7] E. Hansen, „bUnit Documentation,“ 2023. [Online]. Available: https://bunit.dev/docs/getting-started/index.html. [Zugriff am 22 Juni 2023].

This post was written by: