---
title: "bUnitとは？Blazorコンポーネントのテストの書き方とv2移行"
url: "https://www.issoh.co.jp/tech/details/3656/"
published: 2024-09-25
updated: 2026-10-28
categories: ["テスト"]
publisher: "株式会社一創"
---

# bUnitとは？Blazorコンポーネントのテストの書き方とv2移行

bUnit（ビーユニット）は、Blazorコンポーネントをブラウザを起動せずにテストする.NET向けのライブラリです。コンポーネントをメモリ上でレンダリングし、出力されたHTML・クリックなどの操作・状態の変化をxUnitやNUnitの単体テストとして検証します。最新版は2026年10月25日公開の2.13.3です。2025年11月のv2で`TestContext`が`BunitContext`に、`RenderComponent`が`Render`に変わったため、ネット上に多いv1の書き方は2.xでビルドが通りません。この記事のテストコードは、bUnit 2.13.3と.NET SDK 10.0.401のプロジェクトに置いて`dotnet test`が成功することを確認しています。

## まとめ

- bUnitはBlazorの**コンポーネント単体テスト用ライブラリ**。テストランナーではないので、xUnit・NUnit・MSTest・TUnitのいずれかと組み合わせる。Microsoft公式ではなくコミュニティ製（MITライセンス・無料）。
- 入れるNuGetパッケージは`bunit`の1つだけ。2.xは**.NET 8以上**が対象で、テストプロジェクトのSDKは`Microsoft.NET.Sdk.Razor`にする（TUnitだけは例外で`Microsoft.NET.Sdk`のまま）。
- 基本形は`BunitContext`を継承し、`Render<T>()`で描画、`Find`で要素を取り、`MarkupMatches`で比較する。
- v1のコードを2.xでビルドすると、`RenderComponent`はエラーCS0619、`SetParametersAndRender`はCS1061、`FakeNavigationManager`はCS0246で止まる。`TestContext`だけは警告CS0618で通る。
- JSInteropは既定で**Strictモード**。設定していないJS呼び出しがあるとテストが例外で落ちる。
- CSSの見た目や実際のJS実行はbUnitでは確かめられない。そこはPlaywrightなどのE2Eテストで担う。

以下、テストプロジェクトの作成、よく書くテストの型、v1からの移行、E2Eとの分担の順に説明します。

## bUnitの位置づけ：Blazorの単体テストを担うコミュニティ製ライブラリ

Blazorには、Microsoftが提供する公式のコンポーネントテスト用フレームワークがありません。[Microsoft Learnの「Razorコンポーネントをテストする」](https://learn.microsoft.com/ja-jp/aspnet/core/blazor/test?view=aspnetcore-10.0)は、単体テストの手段としてbUnitを紹介したうえで、bUnitはサードパーティ製でMicrosoftがサポート・保守するものではないと注記しています。リポジトリは.NET Foundation配下で、ライセンスはMITです。

bUnit自体はテストを実行しません。テストの発見・実行・結果の出力はxUnitなどの汎用フレームワークが受け持ち、bUnitはその上でコンポーネントの描画・操作・検証の機能を足します。そのため実行は通常の単体テストと同じく`dotnet test`やVisual Studioのテストエクスプローラーで行い、CIにもそのまま載ります。Azure DevOpsでの組み込み方は[Azure Pipelinesの使い方｜YAMLの書き方とCI/CDパイプライン作成手順](/tech/details/3176/)が参考になります。

実ブラウザを使わないので速いのが利点です。本記事に掲載したテスト10件を含む検証用プロジェクト（11件）を、bUnit 2.13.3・.NET SDK 10.0.401の`dotnet test`で実行した記録では、テストの実行時間は合計約1秒でした。Blazor自体の採用判断は[Blazor（ブレイザー）の将来性は？2026年の採用判断と.NET 10・11の動向](/tech/details/7855/)で扱っています。

## bUnitのインストールとテストプロジェクトの作成

bUnitは[NuGetのbunitパッケージ](https://www.nuget.org/packages/bunit)で配布されています。v1では`bunit.core`と`bunit.web`に分かれていましたが、v2で`bunit`に一本化され、旧2パッケージは1.x系のまま更新されません。

### bunit.templateによるテストプロジェクトの新規作成

新しくテストプロジェクトを作るなら、`bunit.template`を使うのが早道です。`--framework`には`xunit`・`xunitv3`・`nunit`・`mstest`を指定でき、省略するとxUnitになります。

```
dotnet new install bunit.template::2.13.3
dotnet new bunit --framework xunitv3 -o BlazorApp.Tests
dotnet sln BlazorApp.sln add BlazorApp.Tests/BlazorApp.Tests.csproj
dotnet add BlazorApp.Tests/BlazorApp.Tests.csproj reference BlazorApp/BlazorApp.csproj
```

2.13.3のテンプレートで生成した`.csproj`は、`TargetFramework`が`net10.0`、SDKが`Microsoft.NET.Sdk.Razor`で、`bunit` 2.13.3と`xunit.v3` 3.2.2を参照していました。`ImplicitUsings`は有効になっていないため、`Task`を使うファイルには`using System.Threading.Tasks;`を自分で書く必要があります。書き忘れるとエラーCS0246でビルドが止まります。

### 既存テストプロジェクトへのbUnit追加

すでにxUnitやNUnitのプロジェクトがある場合は、パッケージを追加してSDKを差し替えます。[公式の作成手順](https://bunit.dev/docs/getting-started/create-test-project.html)では、SDKを`Microsoft.NET.Sdk.Razor`にし、ターゲットを`net8.0`以上にするよう指示しています。Razor SDKにしないと、テスト側に置いた`.razor`ファイルがコンパイルされません。

```
dotnet add package bunit --version 2.13.3

<!-- .csproj の先頭 -->
<Project Sdk="Microsoft.NET.Sdk.Razor">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
```

TUnitにはテンプレートが無く、Razor SDKとも組み合わせられないと公式手順に書かれています。TUnitでは`Microsoft.NET.Sdk`のまま、テストを`.cs`ファイルだけで書くことになります。.NET 6や7のプロジェクトでは2.xを入れられないので、1.x系の最終版1.40.0を使います。.NETのバージョン選びは[.NET 10（LTS）とは？SDKの選び方と入手・サポート期限・C# 14と移行を解説](/tech/details/9954/)を参照してください。

## 最初のテストを書く：Render・Find・MarkupMatches

以降の例は、テンプレートで作った`BlazorApp.Tests`プロジェクトに置く前提です。テンプレートの`.csproj`には`Bunit`・`Bunit.TestDoubles`・`Xunit`・`Microsoft.Extensions.DependencyInjection`の`using`が登録済みなので、足りない分を`GlobalUsings.cs`にまとめておきます。プロジェクト名を変えた場合は、各ファイルの`namespace`も合わせて変えてください。

```
// GlobalUsings.cs
global using System.Threading.Tasks;
global using Microsoft.AspNetCore.Components.Web;
global using Microsoft.JSInterop;
```

テンプレートに入っている`Counter`コンポーネント（ボタンを押すと数が1増える）を例にします。テストクラスは`BunitContext`を継承し、`Render<Counter>()`で描画した結果（テスト対象の意味でcutと呼ぶ慣習があります）から要素を探して操作します。

```
namespace BlazorApp.Tests;

public class CounterTests : BunitContext
{
    [Fact]
    public void ボタンを押すとカウントが1増える()
    {
        var cut = Render<Counter>();

        cut.Find("button").Click();

        cut.Find("p").MarkupMatches("<p>Current count: 1</p>");
    }
}
```

`Find`はCSSセレクターで最初に一致した要素を返し、見つからなければ例外を投げます。`MarkupMatches`は文字列の完全一致ではなく、HTMLとして意味が同じかを比べます。`Counter`の`<p>`の中には改行と空白が入っていますが、意味のない空白は無視されるので上のテストは通ります。`Assert.Equal`で文字列を比べると、この空白の違いで落ちます。

テストは`.razor`ファイルにも書けます。期待するHTMLをRazorの記法で書けるので、マークアップが長いコンポーネントでは読みやすくなります。

```
@inherits BunitContext

@code {
    [Fact]
    public void 初期表示は0()
    {
        var cut = Render(@<Counter />);

        cut.Find("p").MarkupMatches(@<p>Current count: 0</p>);
    }
}
```

xUnit v3にも`TestContext`という同名のクラスがあり、v1の`TestContext`を継承するとどちらを指すのか曖昧になります。テンプレートのコメントにもこの衝突が書かれており、v2で`BunitContext`へ改称された理由の1つです。

## よく書くテストの型：パラメータ・非同期・DI・JSInterop・認証

実際のコンポーネントは、親から値を受け取り、サービスからデータを取り、JavaScriptや認証状態に依存します。ここでは、それぞれの依存をテスト側で用意する書き方を示します。テスト対象は次の5つのコンポーネントと1つのインターフェースで、いずれもテストプロジェクトに置いて動作を確認しています。

```
// IWeatherService.cs
namespace BlazorApp.Tests;
public interface IWeatherService { Task<string> GetSummaryAsync(); }

@* Greeting.razor *@
<p class="greeting">こんにちは、@Name さん</p>
<button @onclick="() => OnSelected.InvokeAsync(Name)">選択</button>
@code {
    [Parameter] public string Name { get; set; } = "";
    [Parameter] public EventCallback<string> OnSelected { get; set; }
}

@* WeatherCard.razor *@
@inject IWeatherService Weather
@if (summary is null) { <p>読み込み中...</p> }
else { <p>今日の天気：@summary</p> }
@code {
    string? summary;
    protected override async Task OnInitializedAsync()
        => summary = await Weather.GetSummaryAsync();
}

@* FocusInput.razor *@
@inject IJSRuntime JS
<input @ref="input" />
@code {
    ElementReference input;
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender) await JS.InvokeVoidAsync("focusElement", input);
    }
}

@* SecretPanel.razor *@
@using Microsoft.AspNetCore.Components.Authorization
<AuthorizeView>
    <Authorized><p>ようこそ、@context.User.Identity?.Name さん</p></Authorized>
    <NotAuthorized><p>ログインしてください</p></NotAuthorized>
</AuthorizeView>

@* OrderButton.razor *@
@inject NavigationManager Nav
<button @onclick='() => Nav.NavigateTo("/orders/42")'>注文詳細へ</button>
```

### Renderによるパラメータとイベントコールバックの指定

`Render<T>`の引数にビルダーを渡し、`Add`でパラメータを指定します。`EventCallback`もラムダ式でそのまま渡せます。描画後にパラメータを変えたいときは、v1の`SetParametersAndRender`ではなく、描画結果の`Render`を呼びます（詳細は[公式のパラメータの渡し方](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html)）。

```
public class GreetingTests : BunitContext
{
    [Fact]
    public void ボタン押下でコールバックに名前が渡る()
    {
        string? selected = null;
        var cut = Render<Greeting>(parameters => parameters
            .Add(p => p.Name, "佐藤")
            .Add(p => p.OnSelected, (string name) => selected = name));

        cut.Find("button").Click();

        Assert.Equal("佐藤", selected);
    }

    [Fact]
    public void パラメータを変えて再描画する()
    {
        var cut = Render<Greeting>(parameters => parameters.Add(p => p.Name, "佐藤"));

        cut.Render(parameters => parameters.Add(p => p.Name, "鈴木"));

        Assert.Contains("鈴木", cut.Find("p.greeting").TextContent);
    }
}
```

### DIによるサービスの差し替えと非同期描画の待機

`BunitContext`の`Services`はBlazor本体と同じ`IServiceCollection`なので、`@inject`しているサービスをテスト用の実装に差し替えられます。外部APIを呼ぶサービスを固定値を返す実装にすれば、表示ロジックだけを検証できます。テスト用の偽実装の作り方は[モックとは？スタブとの違い・テストダブル5分類と5言語の書き方を実行例で解説](/tech/details/13505/)で整理しています。

```
public class WeatherCardTests : BunitContext
{
    sealed class ControlledWeatherService : IWeatherService
    {
        public TaskCompletionSource<string> Completion { get; } = new();
        public Task<string> GetSummaryAsync() => Completion.Task;
    }

    [Fact]
    public void 取得後に天気を表示する()
    {
        var service = new ControlledWeatherService();
        Services.AddSingleton<IWeatherService>(service);

        var cut = Render<WeatherCard>();
        cut.Find("p").MarkupMatches("<p>読み込み中...</p>");

        service.Completion.SetResult("晴れ");

        cut.WaitForAssertion(() =>
            cut.Find("p").MarkupMatches("<p>今日の天気：晴れ</p>"));
    }
}
```

`OnInitializedAsync`でデータを取るコンポーネントは、待っている`Task`が完了するまで「読み込み中」を描画します。`Task.Delay`で遅らせると、マシンの速さ次第で最初の検証より先に完了してしまうため、上の例では`TaskCompletionSource`で完了のタイミングをテスト側から決めています。`WaitForAssertion`は再描画のたびにアサーションを試し、通るまで待ちます。待つ上限は[既定で1秒](https://bunit.dev/docs/interaction/awaiting-async-state.html)で、超えると`WaitForFailedException`で失敗します。CIのマシンが遅くて落ちる場合は、第2引数で`TimeSpan`を渡すか、静的プロパティ`BunitContext.DefaultWaitTimeout`を延ばします。HTTP通信そのものを差し替えたい場合、公式は`RichardSzalay.MockHttp`を使う方法を案内しています。

### JSInteropの設定とStrictモードの挙動

bUnitには`IJSRuntime`の代わりになる実装が最初から組み込まれています。[既定はStrictモード](https://bunit.dev/docs/test-doubles/emulating-ijsruntime.html)で、あらかじめ設定していないJS呼び出しを受けると例外を投げます。たとえば`OnAfterRenderAsync`で`focusElement`を呼ぶコンポーネントを何も設定せずに描画すると、`JSRuntimeUnhandledInvocationException`でテストが落ちます。

```
public class FocusInputTests : BunitContext
{
    [Fact]
    public void 設定していないJS呼び出しは例外になる()
    {
        Assert.Throws<JSRuntimeUnhandledInvocationException>(() => Render<FocusInput>());
    }

    [Fact]
    public void JS呼び出しを登録して呼ばれたことを確かめる()
    {
        JSInterop.SetupVoid("focusElement", _ => true).SetVoidResult();

        Render<FocusInput>();

        JSInterop.VerifyInvoke("focusElement");
    }

    [Fact]
    public void Looseモードなら未設定の呼び出しを無視する()
    {
        JSInterop.Mode = JSRuntimeMode.Loose;

        Render<FocusInput>();
    }
}
```

`SetVoidResult()`を付けないと模擬された呼び出しが完了せず、コンポーネントの`await`の先が実行されません。戻り値のある呼び出しは`JSInterop.Setup<T>("関数名").SetResult(値)`で返す値を決めます。Looseモードは未設定の呼び出しに既定値を返して先へ進むので、サードパーティのUI部品がJSを大量に呼ぶ場合は楽です。ただし、呼ぶべきJSを呼んでいない不具合も素通りします。自分で書いたJS連携はStrictのまま`VerifyInvoke`で呼び出しを確かめる方が安全です。

### 認証状態の設定とNavigationManagerによる遷移先確認

`AuthorizeView`を含むコンポーネントは、`AddAuthorization()`で認証の仕組みを登録してから描画します。何も設定しなければ未ログイン、`SetAuthorized("ユーザー名")`でログイン済み、`SetRoles`でロール付きの状態を作れます（[公式の認証の説明](https://bunit.dev/docs/test-doubles/auth.html)）。

```
public class SecretPanelTests : BunitContext
{
    [Fact]
    public void ログイン済みならユーザー名を表示する()
    {
        var auth = AddAuthorization();
        auth.SetAuthorized("tanaka");

        var cut = Render<SecretPanel>();

        cut.MarkupMatches("<p>ようこそ、tanaka さん</p>");
    }

}

public class OrderButtonTests : BunitContext
{
    [Fact]
    public void ボタンを押すと注文詳細へ遷移する()
    {
        var nav = Services.GetRequiredService<BunitNavigationManager>();

        var cut = Render<OrderButton>();
        cut.Find("button").Click();

        Assert.Equal("http://localhost/orders/42", nav.Uri);
    }
}
```

ページ遷移を伴うボタンのテストでは、`BunitNavigationManager`の`Uri`で遷移先を確かめます。ベースURLは`http://localhost/`です。v1では`FakeNavigationManager`という名前でした。

## v1のコードを2.xでビルドしたときのエラーと置き換え

bUnit 2.0.66（2025年11月11日公開）で、.NET 8未満の対象が外れ、.NET 10が追加され、APIが整理されました。Microsoft Learnのサンプルを含め、検索で見つかる解説の多くはv1の`TestContext`と`RenderComponent`のまま書かれています。そこで、v1の書き方のテストを2.13.3でビルドし、どこで止まるかを確かめました。

| v1の書き方                            | 2.13.3でのビルド結果 | v2の書き方                           |
| --------------------------------- | ------------- | -------------------------------- |
| `: TestContext`                   | 警告CS0618      | `: BunitContext`                 |
| `RenderComponent<T>()`            | エラーCS0619     | `Render<T>()`                    |
| `cut.SetParametersAndRender(...)` | エラーCS1061     | `cut.Render(...)`                |
| `FakeNavigationManager`           | エラーCS0246     | `BunitNavigationManager`         |
| `DisposeComponents()`             | 移行ガイドで改名      | `await DisposeComponentsAsync()` |
| `IRenderedFragment`               | 移行ガイドで削除      | `IRenderedComponent<T>`          |
| `bunit.core`・`bunit.web`          | 1.x止まり        | `bunit`                          |

上4行は実際にビルドした結果、下3行は[公式の1.xから2.xへの移行ガイド](https://bunit.dev/docs/migrations/1to2.html)の記載です。差が出るのは、旧APIの残し方が違うためです。v2.13.3のソースでは、`TestContext`は`BunitContext`を継承したクラスとして残り、`[Obsolete]`の第2引数が`false`（警告のみ）になっています。一方`RenderComponent`は`[Obsolete]`の第2引数が`true`で、呼ぶとコンパイルエラーになります。継承元だけ`BunitContext`に直して`RenderComponent`を残すと、テストは1件も動きません。

移行では、パッケージ参照、対象の.NET、APIの順に確認します。まず`bunit.core`・`bunit.web`の参照を外して`bunit`を入れ、ターゲットを.NET 8以上にします。次にエラーCS0619・CS1061・CS0246が出る箇所を表のとおり置き換え、最後に警告CS0618を見ながら`TestContext`を`BunitContext`へ直します。`MouseEventArgs`の各項目を引数で並べる`Click(detail: 2, ctrlKey: true)`のような呼び方も廃止されたので、`new MouseEventArgs { Detail = 2, CtrlKey = true }`を渡す形に書き換えます。

2.0.66以降の更新は不具合の修正と機能追加が中心です。2.6.2で.NET 11に対応し、2.8.6では`WaitFor`系のヘルパーに非同期のアサーションを渡せるオーバーロードが入りました。最新の2.13.3では、AngleSharp 1.8.6と組み合わせたときに`Find`が`TypeLoadException`を投げる不具合が直っています。変更履歴は[GitHubのCHANGELOG](https://github.com/bUnit-dev/bUnit/blob/main/CHANGELOG.md)で確認できます。

## bUnitとE2Eテスト（Playwright）の分担

bUnitはブラウザを使わないので、CSSが当たった見た目、実際のJavaScriptの動作、ブラウザごとの差は確かめられません。JSInteropも前の章のとおり呼び出しを模擬するだけです。Microsoft Learnは、コンポーネントがどれだけJSに依存するかで手段を分けるよう勧めています。

| コンポーネントの性質             | 向く手段                |
| ---------------------- | ------------------- |
| JSを使わない                | bUnit               |
| フォーカスやアニメーション程度のJS呼び出し | bUnit（JSInteropを設定） |
| 大きなJSライブラリと単純な呼び出しで連携  | bUnit＋JS側の単体テスト     |
| JSによるDOM操作に動作が依存する     | E2Eテスト              |
| 模擬しにくい依存を持つ他社製部品に頼る    | E2Eテスト              |

おすすめの分け方は、条件分岐・入力チェック・状態遷移・権限による表示の出し分けをbUnitで細かく押さえ、E2Eはログインから注文完了までのような主要な画面遷移に絞ることです。bUnitのテストは1件あたりミリ秒単位で動きますが、実行時間や保守の負担は件数と処理内容によって増えます。E2Eは1件ごとにブラウザを動かすため、本数が増えたときのCIの遅れがbUnitよりはるかに大きくなります。

反対に、bUnitを主役にすべきでない場面もあります。画面の大半をサードパーティのUI部品が描いていて、そのJS呼び出しを1つずつ設定しないと描画すら通らない場合です。Looseモードで黙らせてもテストが保証する範囲はごくわずかなので、その画面はE2Eで確かめる方が費用に見合います。PlaywrightのE2Eは[Playwright（プレイライト）とは？仕組み・できること・Seleniumとの違いを解説](/tech/details/3167/)、E2Eの範囲の決め方は[E2E（エンドツーエンド）テストのベストプラクティス｜結合テストとの違いとツール選定](/tech/details/5470/)で解説しています。既存のBlazorアプリにテストを後から足していく体制づくりは、[一草の保守運用・内製化支援](/service/system/maintenance/)でも相談を受けています。

## よくある質問

### bUnitはMicrosoftの公式ツールですか？

公式ではありません。コミュニティが開発するライブラリで、Microsoft LearnもMicrosoftがサポート・保守するものではないと明記しています。ライセンスはMITで、商用プロジェクトでも無料で使えます。

### bUnitのNuGetパッケージはどれを入れればよいですか？

`bunit`の1つだけです。v1時代の`bunit.core`と`bunit.web`は2.xで`bunit`に統合され、1.x系のまま更新が止まっています。新しくプロジェクトを作るなら`bunit.template`のテンプレートを使うと、テストフレームワークの参照まで揃います。

### bUnitはxUnit以外のテストフレームワークでも使えますか？

使えます。NUnit・MSTest・TUnitにも対応しています。テンプレートで選べるのは`xunit`・`xunitv3`・`nunit`・`mstest`の4つで、TUnitは手動で作成します。TUnitはRazor SDKと組み合わせられないため、テストは`.cs`ファイルで書きます。

### 「TestContext は旧形式です」という警告が出るのはなぜですか？

v2で`TestContext`が`BunitContext`に改称され、旧名は警告だけを出す`[Obsolete]`付きで残されているためです。xUnit v3にも同名のクラスがあり、名前の衝突を避ける目的もあります。継承元を`BunitContext`に変えれば警告は消えます。同時に`RenderComponent`も`Render`へ直さないとビルドエラーになります。

### Blazor ServerとBlazor WebAssemblyのどちらもテストできますか？

どちらもテストできます。bUnitの対象はコンポーネント単体で、ホスティングモデルに依存しません。サーバー側のサービスやHTTP通信は、DIで差し替えるか`RichardSzalay.MockHttp`で模擬して検証します。

## 関連記事

- [Blazor（ブレイザー）の将来性は？2026年の採用判断と.NET 10・11の動向](/tech/details/7855/)
- [モックとは？スタブとの違い・テストダブル5分類と5言語の書き方を実行例で解説](/tech/details/13505/)
- [Playwright（プレイライト）とは？仕組み・できること・Seleniumとの違いを解説](/tech/details/3167/)
- [Playwright Component Testingの使い方｜1.62のstory方式と移行手順](/tech/details/7392/)
- [.NET 10（LTS）とは？SDKの選び方と入手・サポート期限・C# 14と移行を解説](/tech/details/9954/)

---

出典: [bUnitとは？Blazorコンポーネントのテストの書き方とv2移行](<https://www.issoh.co.jp/tech/details/3656/>)（株式会社一創）
