bUnit: how to love and not fear Unit testing Blazor Components

Gepubliceerd op: 
22 juni 2026

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).

Terug naar het overzicht

We are hiring

Contract

Azure DevOps / Platform Engineer

Antwerpen Province
Contract

Azure Cloud Engineer

Antwerpen Province
Contract

Azure Data Engineer – met AI Exposure

Antwerpen Province
Contract

DBA SQL

Brussels-Capital Region
Contract

Data Professional (AI-minded)

Antwerpen Province
Contract

.NET AI Developer

Antwerpen Province
Contract

Senior .NET Developer

Flemish Brabant Province
Contract

Medior Nest.JS Engineer

Brussels-Capital Region
Contract

Devops Platform engineer

Brussels-Capital Region

Contacteer ons team

Slechts één bestand.
256 MB limiet.
Toegestane types: gif, jpg, jpeg, png, bmp, eps, tif, pict, psd, txt, rtf, html, odf, pdf, doc, docx, ppt, pptx, xls, xlsx, xml, avi, mov, mp3, mp4, ogg, wav, bz2, dmg, gz, jar, rar, sit, svg, tar, zip.
Ik ga akkoord met het privacybeleid