Why bUnit for Blazor Component Testing
bUnit is a testing library built specifically for Razor components that renders them into an in-memory test renderer without needing a browser, letting you assert on the produced markup, simulate user interactions, and verify component behavior in milliseconds rather than the seconds a browser-based end-to-end test would take. It integrates with xUnit, NUnit, or MSTest as the underlying test runner, so a bUnit test class looks like an ordinary unit test class that additionally inherits from bUnit's TestContext to get access to RenderComponent and a configurable service collection for dependency injection.
Cricket analogy: A batter practicing against a bowling machine in the nets gets instant, repeatable feedback on technique without needing a full XI and a real match, just as bUnit renders a component in memory for instant feedback without spinning up a real browser.
Rendering Components and Making Assertions
A bUnit test typically calls RenderComponent<T>() to render a component, optionally passing parameters via a builder lambda with parameters.Add(c => c.SomeParam, value), and the returned IRenderedComponent exposes both the rendered markup as an HTML string and a strongly-typed Instance property for reaching into the component's public members. Assertions commonly use MarkupMatches, which does a semantic HTML comparison that ignores insignificant whitespace and attribute ordering, making tests resilient to minor markup formatting changes while still catching real regressions in structure or content, which is generally preferable to raw string equality checks against the rendered HTML.
Cricket analogy: Hawk-Eye technology compares a ball's trajectory to the stumps using a tolerance for the exact physical dimensions rather than pixel-perfect exactness, similar to MarkupMatches doing a semantic comparison that ignores insignificant whitespace differences.
Simulating User Interaction
bUnit's rendered component exposes Find and FindAll, backed by AngleSharp, to locate elements by CSS selector, and those elements expose interaction methods like Click(), Input("new value"), and Submit() that raise the corresponding Blazor event and trigger a synchronous re-render before the method returns, so assertions immediately after an interaction see the post-interaction state without manual waiting. For components that depend on cascading values, such as an EditContext or an authentication state, pass them explicitly through RenderComponent's parameter builder using CascadingValue so the component under test behaves as it would inside the real component tree.
Cricket analogy: A DRS review immediately shows the updated decision on the big screen the moment the third umpire rules, with no lag between the review and the outcome, mirroring how a bUnit Click() triggers a synchronous re-render you can assert on immediately.
Mocking Dependencies and Verifying Behavior
Because TestContext exposes a Services collection identical in shape to the app's real IServiceCollection, you register mock implementations of injected services, typically created with a mocking library like Moq or NSubstitute, before rendering the component under test, ensuring the component receives the fake instead of a real HTTP client or database context. For components that call JavaScript via IJSRuntime, bUnit provides a JSInterop object on TestContext where you configure expected calls with SetupVoid or Setup<TResult> and specify the exact identifier and arguments, throwing a clear test failure if the component invokes JS interop that wasn't set up, which catches accidental or unexpected interop calls.
Cricket analogy: A team practices against throwdowns from a coach simulating a specific bowler's pace and line rather than facing the real international bowler in every net session, mirroring how a mocked service stands in for the real dependency during a bUnit test.
public class CounterTests : TestContext
{
[Fact]
public void Clicking_IncrementButton_IncreasesCount()
{
// Arrange
var cut = RenderComponent<Counter>(parameters => parameters
.Add(p => p.InitialValue, 0));
// Act
cut.Find("button#increment").Click();
// Assert
cut.Find("p#count").MarkupMatches("<p id=\"count\">Current count: 1</p>");
}
[Fact]
public void SaveButton_CallsJsAlert_OnSuccess()
{
Services.AddSingleton<IOrderService>(Mock.Of<IOrderService>(s =>
s.SaveAsync(It.IsAny<Order>()) == Task.FromResult(true)));
JSInterop.SetupVoid("alert", "Order saved");
var cut = RenderComponent<OrderForm>();
cut.Find("button#save").Click();
JSInterop.VerifyInvoke("alert");
}
}bUnit tests run against Blazor's actual component lifecycle and rendering pipeline in memory, so behaviors like OnInitializedAsync, OnParametersSet, and IDisposable.Dispose all fire as they would in a browser, which means you're testing real component logic, not a stub or approximation of it.
Forgetting to register a service the component under test depends on via Services.AddSingleton/AddScoped before calling RenderComponent throws an InvalidOperationException at render time rather than failing silently, so always set up the full dependency graph, including nested child components' dependencies, before rendering.
- bUnit renders Razor components in memory using xUnit, NUnit, or MSTest as the test runner, avoiding the overhead of a real browser.
- RenderComponent<T>() renders a component and returns markup plus a strongly-typed Instance for assertions.
- MarkupMatches performs a semantic HTML comparison, tolerating whitespace and attribute-order differences while catching real regressions.
- Find/FindAll use AngleSharp CSS selectors, and interaction methods like Click() trigger a synchronous re-render.
- Register mock services in TestContext.Services before rendering so the component under test uses fakes instead of real dependencies.
- JSInterop.SetupVoid/Setup configure expected JavaScript interop calls and fail the test on unexpected invocations.
- Pass cascading values explicitly via the parameter builder so components depending on EditContext or auth state behave correctly under test.
Practice what you learned
1. What is the main advantage of bUnit over a browser-based end-to-end test for Blazor components?
2. What does MarkupMatches check when asserting on rendered output?
3. What happens when you call cut.Find("button").Click() in a bUnit test?
4. How do you provide a fake implementation of an injected service to a component under test?
5. What does JSInterop.SetupVoid do in a bUnit test?
Was this page helpful?
You May Also Like
Blazor Best Practices
Practical guidelines for structuring components, managing state, and keeping Blazor apps fast and maintainable in production.
Deploying Blazor Apps
How to publish and deploy Blazor WebAssembly and Blazor Server apps to production, including hosting choices, compression, and configuration.
Blazor Interview Questions
Commonly asked Blazor interview questions covering render modes, component lifecycle, state management, JS interop, and performance, with clear answers.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics