Testing Blazor components with bUnit

In this new post, I show a new framework for testing Blazor components called bUnit.

bUnit is a testing library for Blazor Components. Its goal is to make it easy to write comprehensive, stable unit tests. With bUnit, you can:

  • Setup and define components under tests using C# or Razor syntax
  • Verify outcomes using semantic HTML comparer
  • Interact with and inspect components as well as trigger event handlers
  • Pass parameters, cascading values and inject services into components under test
  • Mock IJSRuntime, Blazor authentication and authorization, and others

bUnit builds on top of existing unit testing frameworks such as xUnit, NUnit, and MSTest, which run the Blazor components tests in just the same way as any normal unit test. bUnit runs a test in milliseconds, compared to browser-based UI tests which usually take seconds to run.

Create the first test

First, we have to create a new test project using one of the available frameworks:

and after that, add bUnit to your test project.

Writing tests for Blazor components

So, testing Blazor components is a little different from testing regular C# classes: Blazor components are rendered, they have the Blazor component life cycle during which we can provide input to them, and they can produce output.

Use bUnit to render the component under test, pass in its parameters, inject required services, and access the rendered component instance and the markup it has produced.

Rendering a component happens through bUnit’s TestContext. The result of the rendering is an IRenderedComponent, referred to as a “rendered component”, that provides access to the component instance and the markup produced by the component.

For example, in your Blazor application create a new file HelloWorld.razor with this content

<h1>Hello world from Blazor</h1>

This is a very basic component but we have the chance to render this component and test it. So, in your test class add the following code.

xUnit

using Xunit;
using Bunit;

namespace Bunit.Tests
{
  public class HelloWorldTest
  {
    [Fact]
    public void HelloWorldComponentRendersCorrectly()
    {
      // Arrange
      using var ctx = new TestContext();

      // Act
      var cut = ctx.RenderComponent<HelloWorld>();

      // Assert
      cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
    }
  }
}

nUnit

using Bunit;
using NUnit.Framework;

namespace Bunit.Tests
{
  public class HelloWorldTest
  {
    [Test]
    public void HelloWorldComponentRendersCorrectly()
    {
      // Arrange
      using var ctx = new Bunit.TestContext();

      // Act
      var cut = ctx.RenderComponent<HelloWorld>();

      // Assert
      cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
    }
  }
}

MSTest

using Bunit;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Bunit.Tests
{
  [TestClass]
  public class HelloWorldTest
  {
    [TestMethod]
    public void HelloWorldComponentRendersCorrectly()
    {
      // Arrange
      using var ctx = new Bunit.TestContext();

      // Act
      var cut = ctx.RenderComponent<HelloWorld>();

      // Assert
      cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
    }
  }
}

Code explained

The test above does the following:

  1. Creates a new instance of the disposable bUnit TestContext, and assigns it to the ctx variable using the using var syntax to avoid unnecessary source code indention.
  2. Renders the <HelloWorld> component using TestContext, which is done through the RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>) method. We cover passing parameters to components on the Passing parameters to components page.
  3. Verifies the rendered markup from the <HelloWorld> component using the MarkupMatches method. The MarkupMatches method performs a semantic comparison of the expected markup with the rendered markup.

TestContext is an ambiguous reference – it could mean Bunit.TestContext or Microsoft.VisualStudio.TestTools.UnitTesting.TestContext – so you have to specify the Bunit namespace when referencing TestContext to resolve the ambiguity for the compiler. Alternatively, you can give bUnit’s TestContext a different name during import, e.g.:
using BunitTestContext = Bunit.TestContext;

Passing parameters to components

bUnit comes with a number of ways to pass parameters to components under test:

  1. In tests written in .razor files, passing parameters is most easily done with inside an inline Razor template passed to the Render method, although the parameter passing option available in tests written in C# files is also available here.
  2. In tests written in .cs files, bUnit includes a strongly typed builder. There are two methods in bUnit that allow passing parameters in C#-based test code:
    • RenderComponent method on the test context, which is used to render a component initially.
    • SetParametersAndRender method on a rendered component, which is used to pass new parameters to an already rendered component.

In the following sub sections, we will show both .cs– and .razor-based test code; just click between them using the tabs.

Regular parameters

A regular parameter is one that is declared using the [Parameter] attribute. The following subsections will cover both non-Blazor type parameters, e.g. int and List<string>, and the special Blazor types like EventCallback and RenderFragment.

Non-Blazor type parameters

Let’s look at an example of passing parameters that takes types which are not special to Blazor, i.e.:

public class NonBlazorTypesParams : ComponentBase
{
  [Parameter]
  public int Numbers { get; set; }

  [Parameter]
  public List<string> Lines { get; set; }
}

This can be done like this:

public class NonBlazorTypesParamsTest
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var lines = new List<string> { "Hello", "World" };

    var cut = ctx.RenderComponent<NonBlazorTypesParams>(parameters => parameters
      .Add(p => p.Numbers, 42)
      .Add(p => p.Lines, lines)
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>‘s Add method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type for the value. This makes the builder’s methods strongly typed and refactor-safe.

EventCallback parameters

This example will pass parameters to the following two EventCallback parameters:

public class EventCallbackParams : ComponentBase
{
  [Parameter]
  public EventCallback<MouseEventArgs> OnClick { get; set; }

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

This can be done like this:

  public class EventCallbackParamsTest
  {
    [Fact]
    public void Test()
    {
      using var ctx = new TestContext();

      Action<MouseEventArgs> onClickHandler = _ => { };
      Action onSomethingHandler = () => { };

      var cut = ctx.RenderComponent<EventCallbackParams>(parameters => parameters
        .Add(p => p.OnClick, onClickHandler)
        .Add(p => p.OnSomething, onSomethingHandler)
      );
    }`
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>’s Add method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type of callback method. This makes the builder’s methods strongly typed and refactor-safe.

ChildContent parameters

The ChildContent parameter in Blazor is represented by a RenderFragment. In Blazor, this can be regular HTML markup, it can be Razor markup, e.g. other component declarations, or a mix of the two. If it is another component, then that component can also receive child content, and so forth.

The following subsections have different examples of child content being passed to the following component:

public class ChildContentParams : ComponentBase
{
  [Parameter]
  public RenderFragment ChildContent { get; set; }
}

Passing HTML to the ChildContent parameter

public class ChildContentParams1Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<ChildContentParams>(parameters => parameters
      .AddChildContent("<h1>Hello World</h1>")
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>‘s AddChildContent method to pass an HTML markup string as the input to the ChildContent parameter.

Passing a component without parameters to the ChildContent parameter

To pass a component, e.g. the classic <Counter> component, which does not take any parameters itself, to a ChildContent parameter, do the following:

public class ChildContentParams2Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<ChildContentParams>(parameters => parameters
      .AddChildContent<Counter>()
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>‘s AddChildContent<TChildComponent> method, where TChildComponent is the (child) component that should be passed to the component under test’s ChildContent parameter.

Passing a component with parameters to the ChildContent parameter

To pass a component with parameters to a component under test, e.g. the <Alert> component with the following parameters, do the following:

[Parameter] public string Heading { get; set; }
[Parameter] public AlertType Type { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }

public class ChildContentParams3Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<ChildContentParams>(parameters => parameters
      .AddChildContent<Alert>(alertParameters => alertParameters
        .Add(p => p.Heading, "Alert heading")
        .Add(p => p.Type, AlertType.Warning)
        .AddChildContent("<p>Hello World</p>")
      )
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>‘s AddChildContent<TChildComponent> method, where TChildComponent is the (child) component that should be passed to the component under test. The AddChildContent<TChildComponent> method takes an optional ComponentParameterCollectionBuilder<TComponent> as input, which can be used to pass parameters to the TChildComponent component, which in this case is the <Alert> component.

Passing a mix of Razor and HTML to a ChildContent parameter

Some times you need to pass multiple different types of content to a ChildContent parameter, e.g. both some markup and a component. This can be done in the following way:

public class ChildContentParams4Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<ChildContentParams>(parameters => parameters
      .AddChildContent("<h1>Below you will find a most interesting alert!</h1>")
      .AddChildContent<Alert>(childParams => childParams
        .Add(p => p.Heading, "Alert heading")
        .Add(p => p.Type, AlertType.Warning)
        .AddChildContent("<p>Hello World</p>")
      )
    );
  }
}

Passing a mix of markup and components to a ChildContent parameter is done by simply calling the ComponentParameterCollectionBuilder<TComponent>‘s AddChildContent() methods as seen here.

RenderFragment parameters

RenderFragment parameter is very similar to the special ChildContent parameter described in the previous section, since a ChildContent parameter is of type RenderFragment. The only difference is the name, which must be anything other than ChildContent.

In Blazor, a RenderFragment parameter can be regular HTML markup, it can be Razor markup, e.g. other component declarations, or it can be a mix of the two. If it is another component, then that component can also receive child content, and so forth.

The following subsections have different examples of content being passed to the following component’s RenderFragment parameter:

public class RenderFragmentParams : ComponentBase
{
  [Parameter]
  public RenderFragment Content { get; set; }
}

Passing HTML to a RenderFragment parameter

public class RenderFragmentParams1Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<RenderFragmentParams>(parameters => parameters
      .Add(p => p.Content, "<h1>Hello World</h1>")
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>‘s Add method to pass an HTML markup string as the input to the RenderFragment parameter.

Passing a component without parameters to a RenderFragment parameter

To pass a component such as the classic <Counter> component, which does not take any parameters, to a RenderFragment parameter, do the following:

public class RenderFragmentParams2Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<RenderFragmentParams>(parameters => parameters
      .Add<Counter>(p => p.Content)
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>‘s Add<TChildComponent> method, where TChildComponent is the (child) component that should be passed to the RenderFragment parameter.

Passing a component with parameters to a RenderFragment parameter

To pass a component with parameters to a RenderFragment parameter, e.g. the <Alert> component with the following parameters, do the following:

[Parameter] public string Heading { get; set; }
[Parameter] public AlertType Type { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
public class RenderFragmentParams3Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<RenderFragmentParams>(parameters => parameters
      .Add<Alert>(p => p.Content, alertParameters => alertParameters
        .Add(p => p.Heading, "Alert heading")
        .Add(p => p.Type, AlertType.Warning)
        .AddChildContent("<p>Hello World</p>")
      )
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>‘s Add<TChildComponent> method, where TChildComponent is the (child) component that should be passed to the RenderFragment parameter. The Add<TChildComponent> method takes an optional ComponentParameterCollectionBuilder<TComponent> as input, which can be used to pass parameters to the TChildComponent component, which in this case is the <Alert> component.

Passing a mix of Razor and HTML to a RenderFragment parameter

Some times you need to pass multiple different types of content to a RenderFragment parameter, e.g. both markup and and a component. This can be done in the following way:

public class RenderFragmentParams4Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<RenderFragmentParams>(parameters => parameters
      .Add(p => p.Content, "<h1>Below you will find a most interesting alert!</h1>")
      .Add<Alert>(p => p.Content, childParams => childParams
        .Add(p => p.Heading, "Alert heading")
        .Add(p => p.Type, AlertType.Warning)
        .AddChildContent("<p>Hello World</p>")
      )
    );
  }
}

Passing a mix of markup and components to a RenderFragment parameter is simply done by calling the ComponentParameterCollectionBuilder<TComponent>‘s Add() methods or using the ChildContent() factory methods in ComponentParameterFactory, as seen here.

Templates parameters

Template parameters are closely related to the RenderFragment parameters described in the previous section. The difference is that a template parameter is of type RenderFragment<TValue>. As with a regular RenderFragment, a RenderFragment<TValue> template parameter can consist of regular HTML markup, it can be Razor markup, e.g. other component declarations, or it can be a mix of the two. If it is another component, then that component can also receive child content, and so forth.

The following examples renders a template component which has a RenderFragment<TValue> template parameter:

@typeparam TItem

<div id="generic-list">
  @foreach (var item in Items)
  {
    @Template(item)
  }
</div>

@code 
{
  [Parameter]
  public IEnumerable<TItem> Items { get; set; }

  [Parameter]
  public RenderFragment<TItem> Template { get; set; }
}

Passing HTML-based templates

To pass a template into a RenderFragment<TValue> parameter that just consists of regular HTML markup, do the following:

public class TemplateParams1Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<TemplateParams<string>>(parameters => parameters
      .Add(p => p.Items, new[] { "Foo", "Bar", "Baz" })
      .Add(p => p.Template, item => $"<span>{item}</span>")
    );
  }
}

The examples pass a HTML markup template into the component under test. This is done with the help of a Func<TValue, string> delegate which takes whatever the template value is as input, and returns a (markup) string. The delegate is automatically turned into a RenderFragment<TValue> type and passed to the template parameter.

The example uses the ComponentParameterCollectionBuilder<TComponent>‘s Add method to first add the data to the Items parameter and then to a Func<TValue, string> delegate.

The delegate creates a simple markup string in the example.

Passing a component-based template

To pass a template into a RenderFragment<TValue> parameter, which is based on a component that receives the template value as input (in this case, the <Item> component listed below), do the following:

<span>@Value</span>
@code 
{
  [Parameter]
  public string Value { get; set; }
}
public class TemplateParams2Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<TemplateParams<string>>(parameters => parameters
      .Add(p => p.Items, new[] { "Foo", "Bar", "Baz" })
      .Add<Item, string>(p => p.Template, value => itemParams => itemParams
        .Add(p => p.Value, value)
      )
    );
  }
}

The example creates a template with the <Item> component listed above.

Unmatched parameters

An unmatched parameter is a parameter that is passed to a component under test, and which does not have an explicit [Parameter] parameter but instead is captured by a [Parameter(CaptureUnmatchedValues = true)] parameter.

In the follow examples, we will pass an unmatched parameter to the following component:

public class UnmatchedParams : ComponentBase
{
  [Parameter(CaptureUnmatchedValues = true)]
  public Dictionary<string, object> InputAttributes { get; set; }
}
public class UnmatchedParamsTest
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<UnmatchedParams>(parameters => parameters
      .AddUnmatched("some-unknown-param", "a value")
    );
  }
}

The examples passes in the parameter some-unknown-param with the value a value to the component under test.

Cascading Parameters and Cascading Values

Cascading parameters are properties with the [CascadingParameter] attribute. There are two variants: named and unnamed cascading parameters. In Blazor, the <CascadingValue> component is used to provide values to cascading parameters, which we also do in tests written in .razor files. However, for tests written in .cs files we need to do it a little differently.

The following examples will pass cascading values to the <CascadingParams> component listed below:

@code 
{
  [CascadingParameter]
  public bool IsDarkTheme { get; set; }

  [CascadingParameter(Name = "LoggedInUser")]
  public string UserName { get; set; }
    
  [CascadingParameter(Name = "LoggedInEmail")]
  public string Email { get; set; }
}

Passing unnamed cascading values

To pass the unnamed IsDarkTheme cascading parameter to the <CascadingParams> component, do the following:

public class CascadingParams1Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var isDarkTheme = true;

    var cut = ctx.RenderComponent<CascadingParams>(parameters => parameters
      .Add(p => p.IsDarkTheme, isDarkTheme)
    );
  }
}

The example pass the variable isDarkTheme to the cascading parameter IsDarkTheme using the Add method on the ComponentParameterCollectionBuilder<TComponent> with the parameter selector to explicitly select the desired cascading parameter and pass the unnamed parameter value that way.

Passing named cascading values

To pass a named cascading parameter to the <CascadingParams> component, do the following:

public class CascadingParams2Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var cut = ctx.RenderComponent<CascadingParams>(parameters => parameters
      .Add(p => p.UserName, "Name of User")
    );
  }
}

The example pass in the value Name of User to the cascading parameter with the name LoggedInUser. Note that the name of the parameter is not the same as the property of the parameter, e.g. LoggedInUser vs. UserName. The example uses the Add method on the ComponentParameterCollectionBuilder<TComponent> with the parameter selector to select the cascading parameter property and pass the parameter value that way.

Passing multiple, named and unnamed, cascading values

To pass all cascading parameters to the <CascadingParams> component, do the following:

public class CascadingParams3Test
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var isDarkTheme = true;

    var cut = ctx.RenderComponent<CascadingParams>(parameters => parameters
      .Add(p => p.IsDarkTheme, isDarkTheme)
      .Add(p => p.UserName, "Name of User")
      .Add(p => p.Email, "user@example.com")
    );
  }
}

The example passes both the unnamed IsDarkTheme cascading parameter and the two named cascading parameters (LoggedInUserLoggedInEmail). It does this using the Add method on the ComponentParameterCollectionBuilder<TComponent> with the parameter selector to select both the named and unnamed cascading parameters and pass values to them that way.

Rendering a component under test inside other components

It is possible to nest a component under tests inside other components, if that is required to test it. For example, to nest the <HelloWorld> component inside the <Wrapper> component do the following:

public class NestedComponentTest
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();

    var wrapper = ctx.RenderComponent<Wrapper>(parameters => parameters
      .AddChildContent<HelloWorld>()
    );
    var cut = wrapper.FindComponent<HelloWorld>();
  }
}

The example renders the <HelloWorld> component inside the <Wrapper> component. What is special in both cases is the use of the FindComponent<HelloWorld>() that returns a IRenderedComponent<HelloWorld>. This is needed because the RenderComponent<Wrapper> method call returns an IRenderedComponent<Wrapper> instance, that provides access to the instance of the <Wrapper> component, but not the <HelloWorld>-component instance.

Configure two-way with component parameters (@bind directive)

To set up two-way binding to a pair of component parameters on a component under test, e.g. the Value and ValueChanged parameter pair on the component below, do the following:

@code {
  [Parameter] public string Value { get; set; } = string.Empty;
  [Parameter] public EventCallback<string> ValueChanged { get; set; }
}
public class TwoWayBindingTest
{
  [Fact]
  public void Test()
  {
    using var ctx = new TestContext();
    var currentValue = string.Empty;

    ctx.RenderComponent<TwoWayBinding>(parameters =>
      parameters.Bind(
        p => p.Value,
        currentValue,
        newValue => currentValue = newValue));
  }
}

The example uses the Bind method to setup two-way binding between the Value parameter and ValueChanged parameter, and the local variable in the test method (currentValue). The Bind method is a shorthand for calling the the Add method for the Value parameter and ValueChanged parameter individually.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.