Writing tests for Blazor components
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.
Note
The preview and beta versions of bUnit included an experimental feature for writing tests using two test components, <Fixture>
and <SnapshotTest>
. Since there is now a better way to write tests in .razor
files, the old experimental feature has been moved into a separate project named bunit.web.testcomponents
. It is kept around to not break early adopters, but no additional features or improvements are planned to it.
To learn more, head over to the bunit.web.testcomponents project on GitHub.
Write tests in .cs
or .razor
files
bUnit works with MSTest, NUnit, and xUnit, and it allows you to write the unit tests in either .cs
or .razor
files.
The latter, writing tests in .razor
files, provides an easier way to declare component markup and HTML markup in the tests, so it will most likely be the go-to for many people in the future.
However, the current Razor editor in Visual Studio 2019 does not offer all the code editing features available in the C# editor, and has some formatting bugs on top of that, so that is something to consider if you choose to write tests in .razor
files.
The following sections show how to get started writing tests in either .cs
or .razor
files.
Creating basic tests in .razor
files
Before writing tests in .razor
files, a few things needs to be in place:
- Make sure the test project has the SDK type set to
Microsoft.NET.Sdk.Razor
. Otherwise the Blazor compiler will not translate your.razor
files into runnable code. - Add an
_Imports.razor
file to the test project. It serves the same purpose as_Imports.razor
files in regular Blazor projects. These using statements are useful to add right away:
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using Microsoft.Extensions.DependencyInjection
@using AngleSharp.Dom
@using Bunit
@using Bunit.TestDoubles
Also add an using statement for your general purpose testing framework, e.g. @using Xunit
for xUnit.
With that in place, lets look at a simple example that tests the following <HelloWorld>
component:
<h1>Hello world from Blazor</h1>
@code
{
[Fact]
public void HelloWorldComponentRendersCorrectly()
{
// Arrange
using var ctx = new TestContext();
// Act
var cut = ctx.Render(@<HelloWorld />);
// Assert
cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
}
}
The test above does the following:
- Creates a new instance of the disposable bUnit TestContext, and assigns it to
ctx
the variable using theusing var
syntax to avoid unnecessary source code indention. - Renders the
<HelloWorld>
component using TestContext, which is done through theRender(RenderFragment)
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.
Tip
Learn more about how the semantic HTML/markup comparison in bUnit works, and how to customize it, on the Customizing the semantic HTML comparison page.
Tip
In bUnit tests, we like to use the abbreviation CUT
, short for "component under test", to indicate the component that is being tested. This is inspired by the common testing abbreviation SUT
, short for "system under test".
Secret sauce of '.razor' files tests
The trick employed in these tests is the "inline Razor templates syntax", i.e. where a render fragment is simply created using the @<{HTML tag}>...</{HTML tag}>
notation. In that notation there is no need to do any escaping of e.g. the quotation mark ("
), that is usually associated with working with markup in C# code.
One small caveat to be aware of is that the inline Razor templates syntax only supports one outer element, e.g. this is OK:
@<Foo>
<Bar>
<Baz>...</Baz>
</Bar>
</Foo>
However, this will not work:
@<Foo></Foo>
<Bar></Bar>
There is a simple workaround though: wrap all elements in the special Blazor element <text>
. The <text>
element will not be part of the rendered output, but it provides a simple way to group multiple root elements into a single inline Razor template. E.g.:
@<text>
<Foo></Foo>
<Bar></Bar>
</text>
Remove boilerplate code from tests
We can remove some boilerplate code from each test by making the TestContext implicitly available to the test class, so we don't have to have using var ctx = new Bunit.TestContext();
in every test. This can be done like so:
@inherits TestContext
@code
{
[Fact]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = Render(@<HelloWorld />);
// Assert
cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
}
}
Since xUnit instantiates test classes for each execution of the test methods inside them, and disposes of them after each test method has run, we simply inherit from TestContext, and methods like Render(RenderFragment) can then be called directly from each test. This is seen in the listing above.
Important
All the examples in the documentation explicitly new up a TestContext
, i.e. using var ctx = new TestContext()
. If you are using the trick above and have your test class inherit from TestContext
, you should NOT new up a TestContext
in test methods also.
Simply call the test context's methods directly, as they are available in your test class.
For example, var cut = ctx.Render(@<HelloWorld/>);
becomes var cut = Render(@<HelloWorld/>);
.
Creating basic tests in .cs
files
This is a simple example of writing tests in .cs
files which tests the following <HelloWorld>
component:
<h1>Hello world from Blazor</h1>
using Xunit;
using Bunit;
namespace Bunit.Docs.Samples
{
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>");
}
}
}
The test above does the following:
- Creates a new instance of the disposable bUnit TestContext, and assigns it to the
ctx
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.
Tip
Learn more about how the semantic HTML/markup comparison in bUnit works, and how to customize it, on the Customizing the semantic HTML comparison page.
Tip
In bUnit tests, we like to use the abbreviation CUT
, short for "component under test", to indicate the component that is being tested. This is inspired by the common testing abbreviation SUT
, short for "system under test".
Remove boilerplate code from tests
We can remove some boilerplate code from each test by making the TestContext implicitly available to the test class, so we don't have to have using var ctx = new Bunit.TestContext();
in every test. This can be done like so:
using Xunit;
using Bunit;
namespace Bunit.Docs.Samples
{
public class HelloWorldImplicitContextTest : TestContext
{
[Fact]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = RenderComponent<HelloWorld>();
// Assert
cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
}
}
}
Since xUnit instantiates test classes for each execution of the test methods inside them, and disposes of them after each test method has run, we simply inherit from TestContext, and methods like RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>) can then be called directly from each test. This is seen in the listing above.
Important
All the examples in the documentation explicitly new up a TestContext
, i.e. using var ctx = new TestContext()
. If you are using the trick above and have your test class inherit from TestContext
, you should NOT new up another TestContext
in test methods also.
Simply call the test contest's methods directly, as they are available in your test class.
For example, var cut = ctx.RenderComponent<HelloWorld>();
becomes var cut = RenderComponent<HelloWorld>();
.
Further reading
With the basics out of the way, next we will look at how to pass parameters and inject services into our component under test. After that, we will cover the ways in which we can verify the outcome of a rendering in more detail