Test Razor components in ASP.NET Core Blazor
Note
This isn't the latest version of this article. For the current release, see the .NET 9 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 9 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 9 version of this article.
By Egil Hansen
Testing Razor components is an important aspect of releasing stable and maintainable Blazor apps.
To test a Razor component, the component under test (CUT) is:
- Rendered with relevant input for the test.
- Depending on the type of test performed, possibly subject to interaction or modification. For example, event handlers can be triggered, such as an
onclick
event for a button. - Inspected for expected values. A test passes when one or more inspected values matches the expected values for the test.
Test approaches
Two common approaches for testing Razor components are end-to-end (E2E) testing and unit testing:
Unit testing: Unit tests are written with a unit testing library that provides:
- Component rendering.
- Inspection of component output and state.
- Triggering of event handlers and life cycle methods.
- Assertions that component behavior is correct.
bUnit is an example of a library that enables Razor component unit testing.
E2E testing: A test runner runs a Blazor app containing the CUT and automates a browser instance. The testing tool inspects and interacts with the CUT through the browser. Playwright for .NET is an example of an E2E testing framework that can be used with Blazor apps.
In unit testing, only the Razor component (Razor/C#) is involved. External dependencies, such as services and JS interop, must be mocked. In E2E testing, the Razor component and all of its auxiliary infrastructure are part of the test, including CSS, JS, and the DOM and browser APIs.
Test scope describes how extensive the tests are. Test scope typically has an influence on the speed of the tests. Unit tests run on a subset of the app's subsystems and usually execute in milliseconds. E2E tests, which test a broad group of the app's subsystems, can take several seconds to complete.
Unit testing also provides access to the instance of the CUT, allowing for inspection and verification of the component's internal state. This normally isn't possible in E2E testing.
With regard to the component's environment, E2E tests must make sure that the expected environmental state has been reached before verification starts. Otherwise, the result is unpredictable. In unit testing, the rendering of the CUT and the life cycle of the test are more integrated, which improves test stability.
E2E testing involves launching multiple processes, network and disk I/O, and other subsystem activity that often lead to poor test reliability. Unit tests are typically insulated from these sorts of issues.
The following table summarizes the difference between the two testing approaches.
Capability | Unit testing | E2E testing |
---|---|---|
Test scope | Razor component (Razor/C#) only | Razor component (Razor/C#) with CSS/JS |
Test execution time | Milliseconds | Seconds |
Access to the component instance | Yes | No |
Sensitive to the environment | No | Yes |
Reliability | More reliable | Less reliable |
Choose the most appropriate test approach
Consider the scenario when choosing the type of testing to perform. Some considerations are described in the following table.
Scenario | Suggested approach | Remarks |
---|---|---|
Component without JS interop logic | Unit testing | When there's no dependency on JS interop in a Razor component, the component can be tested without access to JS or the DOM API. In this scenario, there are no disadvantages to choosing unit testing. |
Component with simple JS interop logic | Unit testing | It's common for components to query the DOM or trigger animations through JS interop. Unit testing is usually preferred in this scenario, since it's straightforward to mock the JS interaction through the IJSRuntime interface. |
Component that depends on complex JS code | Unit testing and separate JS testing | If a component uses JS interop to call a large or complex JS library but the interaction between the Razor component and JS library is simple, then the best approach is likely to treat the component and JS library or code as two separate parts and test each individually. Test the Razor component with a unit testing library, and test the JS with a JS testing library. |
Component with logic that depends on JS manipulation of the browser DOM | E2E testing | When a component's functionality is dependent on JS and its manipulation of the DOM, verify both the JS and Blazor code together in an E2E test. This is the approach that the Blazor framework developers have taken with Blazor's browser rendering logic, which has tightly-coupled C# and JS code. The C# and JS code must work together to correctly render Razor components in a browser. |
Component that depends on 3rd party class library with hard-to-mock dependencies | E2E testing | When a component's functionality is dependent on a 3rd party class library that has hard-to-mock dependencies, such as JS interop, E2E testing might be the only option to test the component. |
Test components with bUnit
There's no official Microsoft testing framework for Blazor, but the community-driven project bUnit provides a convenient way to unit test Razor components.
Note
bUnit is a third-party testing library and isn't supported or maintained by Microsoft.
bUnit works with general-purpose testing frameworks, such as MSTest, NUnit, and xUnit. These testing frameworks make bUnit tests look and feel like regular unit tests. bUnit tests integrated with a general-purpose testing framework are ordinarily executed with:
- Visual Studio's Test Explorer.
dotnet test
CLI command in a command shell.- An automated DevOps testing pipeline.
Note
Test concepts and test implementations across different test frameworks are similar but not identical. Refer to the test framework's documentation for guidance.
The following demonstrates the structure of a bUnit test on the Counter
component in an app based on a Blazor project template. The Counter
component displays and increments a counter based on the user selecting a button in the page:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
The following bUnit test verifies that the CUT's counter is incremented correctly when the button is selected:
@code {
[Fact]
public void CounterShouldIncrementWhenClicked()
{
// Arrange
using var ctx = new TestContext();
var cut = ctx.Render(@<Counter />);
var paraElm = cut.Find("p");
// Act
cut.Find("button").Click();
// Assert
var paraElmText = paraElm.TextContent;
paraElm.MarkupMatches("Current count: 1");
}
}
Tests can also be written in a C# class file:
public class CounterTests
{
[Fact]
public void CounterShouldIncrementWhenClicked()
{
// Arrange
using var ctx = new TestContext();
var cut = ctx.RenderComponent<Counter>();
var paraElm = cut.Find("p");
// Act
cut.Find("button").Click();
// Assert
var paraElmText = paraElm.TextContent;
paraElmText.MarkupMatches("Current count: 1");
}
}
The following actions take place at each step of the test:
Arrange: The
Counter
component is rendered using bUnit'sTestContext
. The CUT's paragraph element (<p>
) is found and assigned toparaElm
. In Razor syntax, a component can be passed as a RenderFragment to bUnit.Act: The button's element (
<button>
) is located and selected by callingClick
, which should increment the counter and update the content of the paragraph tag (<p>
). The paragraph element text content is obtained by callingTextContent
.Assert:
MarkupMatches
is called on the text content to verify that it matches the expected string, which isCurrent count: 1
.
Note
The MarkupMatches
assert method differs from a regular string comparison assertion (for example, Assert.Equal("Current count: 1", paraElmText);
). MarkupMatches
performs a semantic comparison of the input and expected HTML markup. A semantic comparison is aware of HTML semantics, meaning things like insignificant whitespace is ignored. This results in more stable tests. For more information, see Customizing the Semantic HTML Comparison.
Additional resources
- Getting Started with bUnit: bUnit instructions include guidance on creating a test project, referencing testing framework packages, and building and running tests.
- How to create maintainable and testable Blazor components - Egil Hansen - NDC Oslo 2022
ASP.NET Core