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.
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 2022 does not offer all the code editing features available in the C# editor, 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>
@inherits TestContext
@code
{
[Fact]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = Render(@<HelloWorld/>);
// Assert
cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
}
}
The test above does the following:
- Inherits from the bUnit TestContext. This base class offers the majority of functions.
- Renders the
<HelloWorld>
component using TestContext, which is done through the Render(RenderFragment)
method. We cover passing parameters to components on the Passing parameters to components page.
- 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.
@inherits BunitTestContext
@code
{
[Test]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = Render(@<HelloWorld />);
// Assert
cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
}
}
Since NUnit instantiates a test class only once for all tests inside it, we cannot simply inherit directly from TestContext as we want a fresh instance of TestContext for each test. Instead, we create a helper class, BunitTestContext
, listed below, and use that to hook into NUnit's [SetUp]
and [TearDown]
methods, which run before and after each test.
using Bunit;
using NUnit.Framework;
public abstract class BunitTestContext : TestContextWrapper
{
[SetUp]
public void Setup() => TestContext = new Bunit.TestContext();
[TearDown]
public void TearDown() => TestContext?.Dispose();
}
The test above does the following:
- Inherits from the
BunitTestContext
above. This base class offers the majority of functions.
- Renders the
<HelloWorld>
component using TestContext, which is done through the Render(RenderFragment)
method. We cover passing parameters to components on the Passing parameters to components page.
- 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.
Alternatively, use the LifeCycle.InstancePerTestCase attribute (introduced in NUnit 3.13) so that a new instance of the test class is created for each test removing the need for the wrapper.
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public class HelloWorldInstancePerTestCase : Bunit.TestContext
{
[Test]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = RenderComponent<HelloWorld>();
// Assert
cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
}
}
@attribute [TestClass]
@inherits BunitTestContext
@code
{
[TestMethod]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = Render(@<HelloWorld/>);
// Assert
cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
}
}
Since MSTest instantiates a test class only once for all tests inside it, we cannot simply inherit directly from TestContext as we want a fresh instance of TestContext for each test. Instead, we create a helper class, BunitTestContext
, listed below, and use that to hook into MSTest's [TestInitialize]
and [TestCleanup]
methods. This runs before and after each test.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Bunit;
public abstract class BunitTestContext : TestContextWrapper
{
[TestInitialize]
public void Setup() => TestContext = new Bunit.TestContext();
[TestCleanup]
public void TearDown() => TestContext?.Dispose();
}
The test above does the following:
- Inherits from the
BunitTestContext
above. This base class offers the majority of functions.
- Renders the
<HelloWorld>
component using TestContext, which is done through the Render(RenderFragment)
method. We cover passing parameters to components on the Passing parameters to components page.
- 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.
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>
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;
public class HelloWorldTest : TestContext
{
[Fact]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = RenderComponent<HelloWorld>();
// Assert
cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
}
}
The test above does the following:
- Inherits from the bUnit's
TestContext
. This base class offers the majority of functions.
- 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 the MarkupMatches
method. The MarkupMatches
method performs a semantic comparison of the expected markup with the rendered markup.
using Bunit;
using NUnit.Framework;
public class HelloWorldTest : BunitTestContext
{
[Test]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = RenderComponent<HelloWorld>();
// Assert
cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
}
}
Since NUnit instantiates a test class only once for all tests inside it, we cannot simply inherit directly from TestContext as we want a fresh instance of TestContext for each test. Instead, we create a helper class, BunitTestContext
, listed below, and use that to hook into NUnit's [SetUp]
and [TearDown]
methods, which run before and after each test.
using Bunit;
using NUnit.Framework;
public abstract class BunitTestContext : TestContextWrapper
{
[SetUp]
public void Setup() => TestContext = new Bunit.TestContext();
[TearDown]
public void TearDown() => TestContext?.Dispose();
}
The test above does the following:
- Inherits from the
BunitTestContext
listed above. This base class offers the majority of functions.
- 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 the MarkupMatches
method. The MarkupMatches
method performs a semantic comparison of the expected markup with the rendered markup.
Alternatively, use the LifeCycle.InstancePerTestCase attribute (introduced in NUnit 3.13) so that a new instance of the test class is created for each test removing the need for the wrapper.
using NUnit.Framework;
namespace Bunit.Docs.Samples;
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public class HelloWorldInstancePerTestCase : Bunit.TestContext
{
[Test]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = RenderComponent<HelloWorld>();
// Assert
cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
}
}
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Bunit;
[TestClass]
public class HelloWorldTest : BunitTestContext
{
[TestMethod]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = RenderComponent<HelloWorld>();
// Assert
cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
}
}
Since MSTest instantiates a test class only once for all tests inside it, we cannot simply inherit directly from TestContext as we want a fresh instance of TestContext for each test. Instead, we create a helper class, BunitTestContext
, listed below, and use that to hook into MSTest's [TestInitialize]
and [TestCleanup]
methods. This runs before and after each test.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Bunit;
public abstract class BunitTestContext : TestContextWrapper
{
[TestInitialize]
public void Setup() => TestContext = new Bunit.TestContext();
[TestCleanup]
public void TearDown() => TestContext?.Dispose();
}
The test above does the following:
- Inherits from the
BunitTestContext
listed above. This base class offers the majority of functions.
- 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 the MarkupMatches
method. The MarkupMatches
method performs a semantic comparison of the expected markup with the rendered markup.
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".
Instantiate TestContext in each test
If you prefer to instantiate TestContext in each test, instead of inheriting from it, you can do so. This can be useful if you have your own base class that you want to inherit from, or if you want to use a different test framework than the ones listed here.
Just be aware that all examples in the rest of the documentation assumes that you are inheriting from TestContext, so adjust accordingly.
public class HelloWorldExplicitContext
{
[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>");
}
}
public class HelloWorldExplicitContext
{
[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>");
}
}
Note
TestContext
is an ambiguous reference - it could mean Bunit.TestContext
or NUnit.Framework.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;
[TestClass]
public class HelloWorldExplicitContext
{
[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>");
}
}
Note
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;
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