Claude Media
Claude CodeでBlazorをbUnitでテストする — コンポーネントの検証ループ

Claude CodeでBlazorをbUnitでテストする — コンポーネントの検証ループ

Claude CodeにBlazorコンポーネントを書かせるとき、bUnitのテストを完了条件にする手順です。テストプロジェクトの作り方、CLAUDE.mdの規約、JSInteropやHttpClientの差し替えまで示します。

Blazorのコンポーネントは、書いたあとにブラウザで開かないと動作が分かりません。Claude Codeに任せると、「それらしく書けたが、ボタンを押したらどうなるかは誰も確かめていない」状態になりがちです。

そこで検証の足場を、bUnitのコンポーネントテストに置きます。コンポーネントを描画し、ボタンを押し、出力されたマークアップを比べる。ここまでをdotnet testで回せば、Claude Codeは結果を読んで自分で直せます。

この記事では、テストプロジェクトの作り方、CLAUDE.mdに書く規約、JavaScript呼び出しやHTTPの差し替え、完了条件の置き方を順に示します。コード例は説明用に組んだ最小の例で、Claudeの実出力ではありません。

bUnitは何を担当し、何を別に用意するのか

bUnitはBlazorコンポーネントのテストライブラリです。テストランナーではありません。bUnitのドキュメントには、xUnit・NUnit・MSTestのような汎用のテストフレームワークが別に必要だと明記されています。

役割は次のように分かれます。

役割担当
テストを見つけて実行し、成否を返す担当xUnit・NUnit・MSTestなど
コンポーネントの描画、パラメータ渡し、サービス注入担当bUnit
テストを起動して結果を読む担当dotnet testとClaude Code

Claude Codeにとっての利点は、結果が文字で返ることです。期待したマークアップと実際のマークアップの差が、失敗メッセージとして出力に載ります。画面を目で見られないClaudeでも、差分を根拠に直せます。

一方で、bUnitが確かめるのは描画されたマークアップと、イベントハンドラーの結果までです。CSSによる見た目の崩れや、実ブラウザでのJavaScriptの動きは対象外です。そこは人が一度見る領域として残します。

Claude Codeにbunitのテストプロジェクトを作らせる

最初に任せるのは、テストプロジェクトの足場です。bUnitにはプロジェクトテンプレートがあり、コマンド2本で作れます。

dotnet new install bunit.template
dotnet new bunit --framework xunit -o MyApp.Tests

--frameworkにはxunit、xunitv3、nunit、mstestを指定できます。省略するとxUnitになります。作ったら、テスト対象のコンポーネントを持つプロジェクトへの参照を足します。

dotnet sln MyApp.sln add MyApp.Tests
dotnet add MyApp.Tests/MyApp.Tests.csproj reference MyApp/MyApp.csproj

テンプレートを使わず手で作る場合の要点は3つです。

  • プロジェクトのSDKをMicrosoft.NET.Sdk.Razorにする。そうしないと、Blazorコンパイラーが.razorファイルを実行可能なコードに変えない
  • TargetFrameworkはnet8.0。bUnitはnet8.0以上で動く
  • dotnet add package bunitでパッケージを足す。ドキュメントの例ではバージョン2.10.3が指定されている

Claudeにこの足場を作らせるときは、生成後に.csprojの1行目を見せてもらうと確実です。SDKがRazorになっていなければ、.razorで書いたテストがコンパイルされず、エラーの原因が分かりにくくなります。

最初のテストは「描画して、押して、読む」の3行にする

bUnitのテストは、BunitContextを継承したクラスで書きます。コンポーネントはRender<T>()で描画し、戻り値は描画済みコンポーネントです。ここから要素を探し、イベントを発火し、マークアップを検証します。

説明用に、押すたびに数が増えるカウンターで書くとこうなります。

<button @onclick="Increment">Clicked @count times</button>
 
@code {
    private int count;
    private void Increment() => count++;
}
using Bunit;
using Xunit;
 
public class CounterTests : BunitContext
{
    [Fact]
    public void ClickIncrementsCount()
    {
        var cut = Render<Counter>();
 
        cut.Find("button").Click();
 
        Assert.Equal("Clicked 1 times", cut.Find("button").TextContent);
    }
}

cutは「component under test」の略で、bUnitのドキュメントでも慣習として使われています。FindにはCSSセレクターを渡します。見つからなければ例外になり、複数ほしいときはFindAllを使います。

イベントの発火は、要素に生えている拡張メソッドです。Click()のほか、各種イベント用のヘルパーがあり、MouseEventArgsを渡す形も選べます。非同期版のClickAsync()もあります。

マークアップは文字列比較でなくMarkupMatchesで比べる

描画結果を文字列で比べると、.razorのインデントや改行まで含まれて壊れやすくなります。bUnitには意味的な比較があり、MarkupMatchesで呼べます。余分な空白や属性の順序の違いを無視して、ブラウザ上で同じ見た目になるかどうかで比べます。

var cut = Render<Heading>();
cut.MarkupMatches("<h3 id=\"heading-1337\">Heading text</h3>");

Claudeに書かせたテストがcut.Markupの生文字列をAssert.Equalしていたら、MarkupMatchesへの書き換えを指示します。理由は、生のマークアップには.razorの空白がそのまま含まれ、ちょっとした整形で失敗するからです。

規約はCLAUDE.mdに、守らせたい確認はhookに置く

テストの書き方の方針は、CLAUDE.mdに書きます。Blazorのテストでは、次のような断片が効きます。

## Blazorコンポーネントのテスト規約
- コンポーネントのテストは tests/MyApp.Tests に bUnit で書く
- 描画は Render<T>()、要素の取得は Find / FindAll を使う
- マークアップの検証は MarkupMatches を使い、cut.Markup の生文字列比較は書かない
- 外部依存は Services に登録し、JS 呼び出しは JSInterop で受ける
- コンポーネントを変更したら dotnet test を実行し、結果を読んでから完了を報告する

ただしCLAUDE.mdは指示であって、強制ではありません。確実に回したいなら、Stop hookにdotnet testを載せます。設定ファイルの形とスクリプトは、同じ.NETのテストを題材にしたClaude CodeでASP.NET Core Web APIを開発する記事に、stop_hook_activeでの無限ループ回避まで含めて載せています。コマンドをdotnet test tests/MyApp.Testsに差し替えれば、bUnitのテストにそのまま使えます。

PostToolUseとStopのどちらに載せるかはhooksによるテスト自動化の使い分けにまとめています。Blazorのテストは描画を伴うぶん、編集のたびに回すと遅く感じることがあります。毎回の編集ではdotnet buildだけ、止まる直前にdotnet test、という分け方が一つの選択肢です。

依存を差し替える — JSInteropとHttpClient

実際のコンポーネントは、JavaScriptを呼んだりHTTPを叩いたりします。bUnitでは、これらをServicesコレクションへの登録とテストダブルで差し替えます。

サービスはレンダリングの前に登録する

Servicesは標準のIServiceCollectionで、本番コードと同じAddSingletonなどが使えます。登録は、コンポーネントを描画する前に行います。AddSingletonを使うには、Microsoft.Extensions.DependencyInjectionの名前空間をインポートします。

using Microsoft.Extensions.DependencyInjection;
 
public class WeatherTests : BunitContext
{
    [Fact]
    public void ShowsForecast()
    {
        Services.AddSingleton<IWeatherService>(new FakeWeatherService());
 
        var cut = Render<WeatherPanel>();
 
        Assert.Contains("Sunny", cut.Markup);
    }
}

登録を忘れて描画すると、コンポーネントが要求するサービスが解決できず、例外になります。Claudeが「描画の順序を入れ替えて直す」方向に走りがちなので、CLAUDE.mdに「登録してから描画」と一行書いておくと無駄な往復が減ります。

JavaScript呼び出しはstrictモードが既定

bUnitはIJSRuntimeの代替として、独自のJSInteropを持っています。モードはstrictとlooseの2つで、既定はstrictです。strictでは、セットアップしていない呼び出しが来ると例外を投げます。looseでは、未設定の呼び出しには型の既定値を返します。

コンポーネントがJavaScriptを呼ぶ場合、次の形で受けます。

JSInterop.SetupVoid("startAnimation");
var cut = Render<Animated>();

呼び出しの記録はInvocationsから読めるので、「このJSを呼んだこと」を検証できます。

ここで注意したいのは、Claudeが失敗を消すためにJSInterop.Mode = JSRuntimeMode.Looseを安易に足すことです。looseにすると例外は消えますが、コンポーネントが意図しないJS呼び出しをしていても気づけません。strictのまま、必要な呼び出しだけをSetupする方針をCLAUDE.mdに書いておくと、テストが本来の検査力を保てます。

HttpClientには標準のモックがない

HttpClientについては、bUnitに組み込みのモックはありません。ドキュメントはRichardSzalay.MockHttpの利用を案内しています。

dotnet add tests/MyApp.Tests package RichardSzalay.MockHttp

ドキュメントには、BunitServiceProviderに拡張メソッドを足して、モックのHttpClientをServicesに登録する補助クラスが載っています。Claudeにはこの補助クラスをテストプロジェクトに追加させ、リクエストごとの応答をテストごとに設定させます。モックのライブラリ選びについては、bUnitは特定のものを推奨しておらず、Moq・JustMock・NSubstituteなども使えるとされています。

Claudeに回させる検証ループ

ここまでを合わせた、1回分のやり取りは次の流れです。

手順

コンポーネント変更の検証ループ

  1. 1

    テストを先に書く

    期待する描画結果とイベント後の状態を、Render・Find・MarkupMatchesで書かせます。

  2. 2

    dotnet testを実行する

    失敗メッセージには期待と実際のマークアップが出ます。Claudeはこれを読みます。

  3. 3

    コンポーネントを直す

    テストは変えず、コンポーネント側を直させます。テストを緩めて通す動きは、diffで確認します。

  4. 4

    全体のテストを回す

    対象のテストが通ったら、プロジェクト全体のdotnet testで既存コンポーネントへの影響を確かめます。

3段目の「テストは変えない」は、プロンプトでも明示しておく価値があります。失敗したテストの期待値を実装に合わせて書き換えると、緑にはなりますが何も保証されません。完了を条件づける考え方はagent hookで「テストが通るまで止めない」を検証させる記事でも扱っています。

つまずきやすい点

bUnitのドキュメントに書かれている挙動のうち、Claudeが書いたテストで踏みやすいものを挙げます。

  • 古い要素を握り続ける: Findで取った要素は、コンポーネントが再描画されると更新されます。一方、NodesからたどったDOMノードは更新されません。要素を保持して使い回すより、必要なときにFindし直すほうが安全です。上のカウンターの例でFindを2回呼んだのもこのためです
  • 非同期ヘルパーは再描画を待たない: ClickAsync()などはイベントハンドラーのコールバックを待ちますが、それが引き起こす再描画までは待ちません
  • NUnitではインスタンスの扱い: [FixtureLifeCycle(LifeCycle.InstancePerTestCase)]を付けると、テストごとに新しいインスタンスが作られます。ドキュメントはNUnit 3.13以降で使える手段として示しています
  • TUnitは別扱い: 事前に用意されたテンプレートはなく、Microsoft.NET.Sdk.RazorのSDKも使えないとされています。TUnitを使う場合は、ドキュメントの個別の説明に沿って構成します

テストを.razorファイルで書く方法もあります。マークアップをそのまま書けますが、Visual Studio 2022のRazorエディターはC#エディターほど機能が揃っていないと、ドキュメント自身が注記しています。Claude Codeに任せるなら、構文エラーが読みやすい.csで書くほうが扱いやすい場面が多いでしょう。

まとめ

Blazorのコンポーネントは、画面を見ずにはテストできないと思われがちです。bUnitを使えば、描画・操作・マークアップの比較までをdotnet testの出力に変えられます。Claude Codeに渡す検証は「テストが通ること」と決め、テストを緩めないこと、JSInteropをstrictに保つこと、サービスを描画前に登録することの3つをCLAUDE.mdに固定すれば、確認の大半を人の手から外せます。見た目の最終確認だけが、人が一度ブラウザで見る仕事として残ります。

この記事を共有:XはてブLinkedIn