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 TestIRenderedComponent
, 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:
- Creates a new instance of the disposable bUnit Test
Context , and assigns it to thectx
variable using theusing var
syntax to avoid unnecessary source code indention. - 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. - Verifies the rendered markup from the
<HelloWorld>
component using theMarkupMatches
method. TheMarkupMatches
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:
- In tests written in
.razor
files, passing parameters is most easily done with inside an inline Razor template passed to theRender
method, although the parameter passing option available in tests written in C# files is also available here. - 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
A 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 (LoggedInUser
, LoggedInEmail
). 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.