Writing Tests in C# 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 - a IRenderedComponent<TComponent> - provides access to the component instance and the markup produced by the component.
Creating a Basic Test
This is a simple example that 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>");
}
}
}
using Bunit;
using NUnit.Framework;
namespace Bunit.Docs.Samples
{
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>");
}
}
}
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;
using Bunit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Bunit.Docs.Samples
{
[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>");
}
}
}
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;
The test above does the following:
- Creates a new instance of the disposable bUnit TestContext, and assigns it to
ctx
variable using the using var
syntax to avoid unnecessary source code indention.
- Renders the
<HelloWorld>
component using TestContext, which is done through the RenderComponent<TComponent>(ITestRenderer, 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".
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 this:
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>(ITestRenderer, Action<ComponentParameterCollectionBuilder<TComponent>>) can then be called directly from each test. This is seen in the listing above.
using Bunit;
using NUnit.Framework;
namespace Bunit.Docs.Samples
{
public class HelloHelloWorldImplicitContextTest : BunitTestContext
{
[Test]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = RenderComponent<HelloWorld>();
// Assert
cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
}
}
}
using System;
using Bunit;
using Bunit.Rendering;
using Microsoft.AspNetCore.Components;
using NUnit.Framework;
namespace Bunit.Docs.Samples
{
public abstract class BunitTestContext : IDisposable
{
private Bunit.TestContext _context;
public ITestRenderer Renderer => _context?.Renderer ?? throw new InvalidOperationException("NUnit has not started executing tests yet");
public TestServiceProvider Services => _context?.Services ?? throw new InvalidOperationException("NUnit has not started executing tests yet");
public void Dispose()
{
_context?.Dispose();
_context = null;
}
[SetUp]
public void Setup() => _context = new Bunit.TestContext();
[TearDown]
public void TearDown() => Dispose();
public IRenderedComponent<TComponent> RenderComponent<TComponent>(params ComponentParameter[] parameters) where TComponent : IComponent
=> _context?.RenderComponent<TComponent>(parameters) ?? throw new InvalidOperationException("NUnit has not started executing tests yet");
public IRenderedComponent<TComponent> RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>> parameterBuilder) where TComponent : IComponent
=> _context?.RenderComponent<TComponent>(parameterBuilder) ?? throw new InvalidOperationException("NUnit has not started executing tests yet");
}
}
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 above, and use that to hook into NUnit's [SetUp]
and [TearDown]
methods, which runs before and after each test.
Then methods like RenderComponent<TComponent>(ITestRenderer, Action<ComponentParameterCollectionBuilder<TComponent>>) can be called directly from each test, as seen in the listing above.
using Bunit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Bunit.Docs.Samples
{
[TestClass]
public class HelloHelloWorldImplicitContextTest : BunitTestContext
{
[TestMethod]
public void HelloWorldComponentRendersCorrectly()
{
// Act
var cut = RenderComponent<HelloWorld>();
// Assert
cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
}
}
}
using System;
using Bunit;
using Bunit.Rendering;
using Microsoft.AspNetCore.Components;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Bunit.Docs.Samples
{
public abstract class BunitTestContext : IDisposable
{
private Bunit.TestContext _context;
public ITestRenderer Renderer => _context?.Renderer ?? throw new InvalidOperationException("MSTest has not started executing tests yet");
public TestServiceProvider Services => _context?.Services ?? throw new InvalidOperationException("MSTest has not started executing tests yet");
public void Dispose()
{
_context?.Dispose();
_context = null;
}
[TestInitialize]
public void Setup() => _context = new Bunit.TestContext();
[TestCleanup]
public void TearDown() => Dispose();
public IRenderedComponent<TComponent> RenderComponent<TComponent>(params ComponentParameter[] parameters) where TComponent : IComponent
=> _context?.RenderComponent<TComponent>(parameters) ?? throw new InvalidOperationException("MSTest has not started executing tests yet");
public IRenderedComponent<TComponent> RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>> parameterBuilder) where TComponent : IComponent
=> _context?.RenderComponent<TComponent>(parameterBuilder) ?? throw new InvalidOperationException("MSTest has not started executing tests yet");
}
}
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 above, and use that to hook into MSTest's [TestInitialize]
and [TestCleanup]
methods. This runs before and after each test.
Then methods like RenderComponent<TComponent>(ITestRenderer, Action<ComponentParameterCollectionBuilder<TComponent>>) can be called directly from each test, as 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 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 ways we can verify the outcome of a rendering in more detail