bUnit: how to love and not fear Unit testing Blazor Components
Unit testing is a cornerstone of your program. There has already been a lot written about why you should add unit tests to your project.
xUnit, NUnit, or MSTest are good general-purpose testing frameworks. Perfect for testing your database, services, APIs, and business logic. But they are created for plain code, and cannot render a Blazor component, simulate a button click in the DOM, or check the component state changes.
Playwright is a great end-to-end testing framework. It automates real browsers to test your application the way an actual user would — clicking buttons, filling forms, navigating pages, and so on. But that also means it spins up the entire application and a real browser for every run. Tests takes longer. You are doing integration testing, not unit testing, and do not have the ability to test a single component in isolation.
But how do you unit test a Blazor Component?
Enter bUnit.
What is bUnit
bUnit is a testing library specifically built for Blazor components.
It lets you unit test Blazor components in isolation — without needing a real browser or running a full server. bUnit runs a test in milliseconds, compared to browser-based UI tests which usually take seconds to run.
It is not a new testing framework. It is an extension on top of the existing unit test frameworks and works great with xUnit, NUnit, or even MSTest.
With bUnit you can:
- Render and inspect Blazor components using C# or Razor syntax
- Pass parameters and cascading values into components under test
- Inject services and mock dependencies like `IJSRuntime` or `HttpClient`
- Trigger event handlers and interact with the rendered output
- Verify outcomes using a semantic HTML comparer
- Mock Blazor authentication and authorization
So how do you use it
First you need to install it or add it to an existing test project.
Adding bUnit to an existing test project
Run the following command or use the Package Manager to add bUnit:
```
dotnet add package bunit
```
Then change the Project SDK in your `.csproj` to `Microsoft.NET.Sdk.Razor`:
```xml
<Project Sdk="Microsoft.NET.Sdk.Razor">
```
This is required so that the project can compile `.razor` files used in tests.
Creating a new bUnit project from the template
For a new project, you can install the bUnit template and then create a new bUnit project with your favourite unit test framework.
```
dotnet new install bunit.template
```
```
dotnet new bunit --framework <xunit || xunitv3 || nunit || mstest> -o MyProject.Tests
```
Now that you have set up a test project with bUnit, it is time to write some tests.
Testing
Let us test the Counter page from the default Blazor examples.
```csharp
public class CounterTest : BunitContext
{
[Fact]
public void CounterShouldIncrementWhenClicked()
{
// Act
var cut = Render<Counter>();
var button = cut.Find("button");
button.Click();
var p = cut.Find("p");
// Assert
p.InnerHtml.MarkupMatches("Current count: 1");
}
}
```
First we render the `Counter` component using the bUnit `Render<T>()` method. The result — commonly called `cut` (Component Under Test) — gives us a handle to inspect and interact with the rendered output.
Next we find the button using `Find("button")`. Since there is only one button on the page, this returns it directly. We then call `Click()` on it, which triggers the component's click handler and increments the counter.
Finally, we find the paragraph element and verify its content using `MarkupMatches()`. This method performs a semantic HTML comparison, meaning it ignores insignificant whitespace differences and focuses on the actual markup content.
Passing parameters
Components rarely live in isolation. You pass parameters to a component under test using a fluent builder inside the `Render<T>()` call. The `.Add()` method uses a lambda to select the parameter by name, so it is fully type-safe and refactor-friendly.
Here is a real example from the `DropdownMultiSelect` component test. It passes a list of items, two selector functions, and a placeholder string:
```csharp
var items = FruitAndVegetables.Get;
var cut = Render<DropdownMultiSelect<TestItem, int>>(parameters => parameters
.Add(c => c.Items, items)
.Add(c => c.ItemKey, c => c.Id)
.Add(c => c.ItemLabel, c => c.Name)
.Add(c => c.Placeholder, "Select items..."));
```
You can pass any type: primitives, collections, delegate functions, and even `EventCallback` parameters:
```csharp
List<int> selectedValues = [1, 2];
Action<List<int>> onChanged = values => selectedValuesThroughCallback = values;
var cut = Render<DropdownMultiSelect<TestItem, int>>(parameters => parameters
.Add(c => c.Items, items)
.Add(c => c.ItemKey, c => c.Id)
.Add(c => c.ItemLabel, c => c.Name)
.Add(c => c.SelectedValues, selectedValues)
.Add(c => c.SelectedValuesChanged, onChanged));
```
Injecting services
If your component depends on a registered service, you register a fake implementation on the test context **before** rendering. bUnit resolves it the same way Blazor's normal DI container would.
Here is an example from the `Weather` component test. The component injects `IWeatherService` to load forecasts asynchronously. In the test we swap in a `WeatherFakeService` that returns controlled test data:
```csharp
public class WeatherTest : BunitContext
{
[Fact]
public void DisplaysLoadingMessageInitially()
{
// Arrange — inject a fake that delays 10 seconds so the component stays in the loading state
Services.AddSingleton<IWeatherService>(
new WeatherFakeService(ForecastsFakeData.TestForecasts(), TimeSpan.FromSeconds(10)));
// Act
var cut = Render<Weather>();
// Assert — component should still be showing the loading message
Assert.Contains("Loading...", cut.Markup);
}
[Fact]
public void DisplaysForecastTable_WhenDataLoads()
{
// Arrange — inject a fake that returns data immediately (no delay)
Services.AddSingleton<IWeatherService>(
new WeatherFakeService(ForecastsFakeData.TestForecasts()));
var cut = Render<Weather>();
// Assert — wait for the async load to complete, then check the table is rendered
cut.WaitForAssertion(() =>
{
var table = cut.Find("table");
Assert.NotNull(table);
});
}
}
```
The `WeatherFakeService` is a small helper class that implements `IWeatherService`. By accepting an optional `TimeSpan` delay you can test both the loading state and the loaded state with the same fake:
```csharp
internal class WeatherFakeService(IEnumerable<WeatherForecast> forecasts, TimeSpan? delay = null)
: IWeatherService
{
public async Task<IEnumerable<WeatherForecast>> GetAsync()
{
if (delay.HasValue)
await Task.Delay(delay.Value);
return forecasts;
}
}
```
Notice `WaitForAssertion()` — but why do you need it at all?
When a component awaits something, `Render<T>()` returns immediately after the first synchronous render, before the async work completes. The component then re-renders once the awaited task finishes, but that happens on a background thread. Without `WaitForAssertion()`, your assertion would run too early and see the loading state instead of the loaded data.
`WaitForAssertion()` keeps retrying the assertion until it passes or a configurable timeout expires. This means you never have to sprinkle `await Task.Delay()` through your tests or guess how long an operation takes.
That is not all
What you have seen so far only scratches the surface. bUnit ships with built-in support for many more testing scenarios:
JS Interop mocking stub out `IJSRuntime` calls so your components can invoke JavaScript without a real browser. You can verify that the right JS function was called with the right arguments. Note that bUnit does not execute actual JavaScript — it only intercepts and verifies the calls made to `IJSRuntime`.
*Use case:* a `ToastButton` component calls `myModule.showToast` when clicked. You want to assert it passes the correct message.
```csharp
JSInterop.SetupVoid("myModule.showToast", _ => true);
var cut = Render<ToastButton>();
cut.Find("button").Click();
JSInterop.VerifyInvoke("myModule.showToast")
.Arguments[0]
.ShouldBe("Saved!");
```
Authentication and Authorization set up a fake authentication state to test how your components behave for signed-in users, anonymous users, or users with specific roles and claims.
*Use case:* an `AdminPanel` component should only show its content to users in the `Admin` role.
```csharp
this.AddAuthorization().WithAuthenticatedUser(user =>
user.AddRoles("Admin"));
var cut = Render<AdminPanel>();
cut.Find(".admin-controls").ShouldNotBeNull();
```
Cascading parameters pass cascading values down the component tree just as you would in a real Blazor app, so child components receive the context they expect.
*Use case:* a `ThemeAwareCard` component reads a cascading `ThemeState` to apply a dark CSS class.
```csharp
var cut = Render<ThemeAwareCard>(parameters => parameters
.AddCascadingValue(new ThemeState { IsDark = true }));
cut.Find(".card").ClassList.ShouldContain("dark");
```
-File uploads simulate file input with the `InputFile` component to test upload workflows without touching the file system.
*Use case:* a `FileUploader` component should display the selected file name after a file is chosen.
```csharp
var cut = Render<FileUploader>();
cut.FindComponent<InputFile>().UploadFiles(
InputFileContent.CreateFromText("file contents", "report.txt"));
Assert.Contains("report.txt", cut.Markup);
```
HttpClient mocking inject a fake `HttpClient` to test components that fetch data from an API, without making real network requests.
*Use case:* a `ProductList` component fetches products from `/api/products` and renders them as a list. Using [`RichardSzalay.MockHttp`](https://github.com/richardszalay/mockhttp) you can control exactly what the API returns.
```csharp
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("/api/products")
.Respond("application/json", "[{\"name\":\"Widget\"}]");
Services.AddSingleton(
new HttpClient(mockHttp) { BaseAddress = new Uri("http://localhost") });
var cut = Render<ProductList>();
cut.WaitForAssertion(() => cut.FindAll("li").Count.ShouldBe(1));
```
NavigationManager mock navigation so you can assert that a component triggers the correct redirect or route change.
*Use case:* a `LoginForm` component should redirect to `/dashboard` after a successful login.
```csharp
var cut = Render<LoginForm>();
cut.Find("[name=username]").Change("alice");
cut.Find("[name=password]").Change("secret");
cut.Find("form").Submit();
var navMan = Services.GetRequiredService<FakeNavigationManager>();
Assert.Equal("http://localhost/dashboard", navMan.Uri);
```
Persistent Component State test components that rely on Blazor's persistent state mechanism during server-side pre-rendering.
*Use case:* a `MyComponent` component persists its counter value during pre-rendering and should restore it on the client. You can seed the state before rendering to verify the component picks it up correctly, without needing a real pre-render cycle.
For general mocking needs beyond these built-in doubles, bUnit works seamlessly with popular mocking libraries like **Moq**, **NSubstitute**, and JustMock.
The full documentation with examples for all of these features is available at [bunit.dev](https://bunit.dev).