Claude Media
Claude CodeでASP.NET Core Web APIを開発する — 統合テストで検証

Claude CodeでASP.NET Core Web APIを開発する — 統合テストで検証

Claude CodeでASP.NET Core Web APIを書かせるとき、WebApplicationFactoryの統合テストをStop hookで毎回走らせ、DB差し替えや認証モックまで検証する設定を示します。

ClaudeにASP.NET Core Web APIを書かせるときは、WebApplicationFactory を使った統合テストを完了条件にします。DI(依存性注入)の登録漏れやルーティングの食い違いは、コンパイルが通っても起動するまで分かりません。実際のリクエストをインメモリで流すテストを dotnet test で回し、その結果を読ませれば、Claude自身が壊れ方を見て直せます。

ここではMicrosoftの統合テスト解説をもとに、テストプロジェクトの土台、CLAUDE.mdの書き方、DBと認証の差し替え、Stop hookによる自動検証までをつなぎます。

統合テストは何を検証し、どこまで書かせるか

統合テストは、2つ以上のコンポーネントが組み合わさって期待どおりの結果を返すかを確かめるテストです。対象になるものはデータベース、ファイルシステム、ネットワーク機器、リクエスト・レスポンスのパイプラインを挙げています。テスト対象のアプリはSUT(System Under Test)と呼ばれます。

単体テストと違い、本番と同じコンポーネントを使い、コード量も実行時間も増えます。そのため、単体テストでも統合テストでも確かめられる挙動は、単体テストを選ぶのが原則です。データアクセスの全組み合わせを統合テストで網羅することも勧めていません。読み取り・書き込み・更新・削除を絞って確かめれば足ります。

Claudeに任せるときも同じ線引きが効きます。「新しいエンドポイントを足したら、統合テストを1本書く」までは指示してよい範囲です。分岐ごとに統合テストを増やさせると、実行時間だけが伸びます。

テストを動かす仕組みは Microsoft.AspNetCore.Mvc.Testing パッケージが担います。このパッケージは次の3つを引き受けます。

  • SUTの依存関係ファイル(.deps)をテストプロジェクトの bin へコピーする
  • コンテンツルートをSUTのプロジェクトルートに設定する
  • TestServer でSUTを起動する WebApplicationFactory クラスを提供する

つまり、アプリ全体をインメモリのテストサーバー上で起動できます。

テストプロジェクトを作る

単体テストと統合テストは別プロジェクトに分けます。分けておくと、インフラ用のテスト部品が単体テストに混ざらず、実行するテスト群も選べます。

dotnet new xunit -o tests/Api.IntegrationTests
dotnet add tests/Api.IntegrationTests reference src/Api
dotnet add tests/Api.IntegrationTests package Microsoft.AspNetCore.Mvc.Testing

プロジェクト名 Api は例です。テストプロジェクトに求められる条件は2つあります。Microsoft.AspNetCore.Mvc.Testing を参照することと、プロジェクトファイルでWeb SDK(Microsoft.NET.Sdk.Web)を指定することです。dotnet new xunit が生成する .csproj の先頭は通常のSDKなので、Web SDKへの書き換えはClaudeに任せず、最初に自分で確認しておくと迷いません。サンプルはxUnitで、xunit.runner.visualstudio 2.4.2以降を使う場合は Microsoft.NET.Test.Sdk の参照も要ります。

最小APIのようにトップレベル文で書かれたアプリは、暗黙に定義される Program クラスをテストから見えるようにする必要があります。方法は2つあります。

  1. SUTの .csproj で内部型をテストプロジェクトに公開する
  2. Program.cs の末尾に public partial class Program { } を足す

サンプルは2番目を採っています。Claudeに最初のエンドポイントを書かせる段階で、この1行を入れさせておきます。

最初のテストは、BasicTests をJSON API向けに読み替えた形になります。サンプルはRazor Pagesなので text/html を確かめていますが、Web APIなら application/json です。以下は読み替えの一例で、サンプルのコードそのものではありません。

public class TodoApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
 
    public TodoApiTests(WebApplicationFactory<Program> factory)
        => _factory = factory;
 
    [Theory]
    [InlineData("/api/todos")]
    public async Task Get_ReturnsSuccessAndJson(string url)
    {
        // Arrange
        var client = _factory.CreateClient();
 
        // Act
        var response = await client.GetAsync(url);
 
        // Assert
        response.EnsureSuccessStatusCode(); // 200-299
        Assert.Equal("application/json; charset=utf-8",
            response.Content.Headers.ContentType?.ToString());
    }
}

CreateClient() が返す HttpClient は、既定でリダイレクトを自動で追い、Cookieも扱います。Arrange・Act・Assertの3段に分けて書かせると、失敗時のログからどの段で落ちたかが読み取れます。

CLAUDE.mdに書く規約

テストの置き場所と書き方を、CLAUDE.mdに固定します。次は一例です。

## ASP.NET Core API の開発ルール
- 統合テストは tests/Api.IntegrationTests に置く。単体テストと混ぜない
- 新しいエンドポイントには WebApplicationFactory を使う統合テストを1本添える
- DB は本番と別にする。CustomWebApplicationFactory で差し替え、実DBに接続しない
- 完了前に `dotnet test tests/Api.IntegrationTests --nologo` を実行し、失敗が残るなら直す
- Program.cs の末尾の `public partial class Program { }` を削除しない

最後の1行が効きます。Program.csの整理中に、使われていないように見える空のpartialクラスが消えると、テストプロジェクトがSUTを参照できなくなります。

DB・設定・サービスをテスト用に差し替える

統合テストの多くは、本番と違うDBや設定で動かします。方法は WebApplicationFactory<TEntryPoint> を継承したファクトリで ConfigureWebHost をオーバーライドし、サービス登録を差し替えるものです。サンプルでは、既存のDbContext登録と DbConnection の登録をサービスコレクションから外し、開いたままの SqliteConnection("DataSource=:memory:") を登録し直しています。

接続を開いたまま保つのは、EFが接続を自動で閉じないようにするためです。EF CoreのインメモリDBプロバイダーは限定的なテストなら使えますが、インメモリ用途ではSQLiteプロバイダーが推奨されています。DBの挙動を実運用に近づけたいなら、SQLiteのインメモリ接続を選びます。

差し替えの落とし穴は順序です。テスト側の ConfigureServices は、アプリの Program.cs が実行された後に走ります。この順序のおかげで、テスト側の登録が本番の登録を上書きできます。

用途別の手段は次のとおりです。

やりたいこと使う手段効く範囲
全テストでDBを差し替える使う手段ファクトリの ConfigureWebHost効く範囲ファクトリ全体
1つのテストだけ別サービスにする使う手段WithWebHostBuilder + ConfigureTestServices効く範囲そのテスト
設定値をテスト用に入れる使う手段ConfigureAppConfiguration + AddInMemoryCollection効く範囲ファクトリ全体
環境名を変える使う手段ファクトリ内の builder.UseEnvironment(...)効く範囲ファクトリ全体

WithWebHostBuilder は、さらに調整した新しい WebApplicationFactory を作ります。外部APIを呼ぶサービスをスタブへ置き換えるテストが、サンプルにあります。テストごとにスコープが閉じるので、スタブが他のテストへ漏れません。

設定値の注入には三つの経路があります。ConfigureHostConfiguration は CreateHost を上書きして使いますが、エントリポイントへは args 経由で渡るため、WebApplication.CreateBuilder(args) に args を渡していないと設定が効きません。ConfigureAppConfiguration は設定源が保たれる反面、アプリが組み上がった後でないと読めません。.NET 11 Preview 7以降には ConfigureWebApplicationBuilder も追加されています。Claudeに設定を足させたのにテストで読めないときは、この2点を疑うと早く片付きます。

環境が未設定のSUTは Development で動きます。本番用の分岐を持つアプリでは、ファクトリで明示的に環境を指定させます。

認証付きエンドポイントを検証させる

認証が絡むエンドポイントには、モック認証で検証する手順が用意されています。流れは次のとおりです。

  1. AllowAutoRedirect = false の WebApplicationFactoryClientOptions でクライアントを作り、未認証のリクエストがサインイン画面へリダイレクトされることを確かめる
  2. WithWebHostBuilder の ConfigureTestServices でテスト用の認証スキーム("TestScheme")を登録する
  3. クライアントの Authorization ヘッダーにそのスキームを付け、認証済みユーザーとして要求する

サンプルはRazor Pagesの /SecurePage を対象にしており、Web APIでは未認証が401になるのが一般的です。リダイレクトの代わりにステータスコードを確かめる形へ読み替えさせます。HTTPSリダイレクトのミドルウェアを使っているときは、BaseAddress = new Uri("https://localhost") を指定すると警告が出なくなります。

Stop hookで統合テストを毎回走らせる

CLAUDE.mdの指示は守られない回もあります。確実に回したいなら、Stop hookにします。Stop hookは終了コード2で終わると、Claudeが止まるのを防ぎ、会話を続けさせます。メッセージにはhookのstderrが使われます。

.claude/settings.json の例です。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/run-api-tests.sh"
          }
        ]
      }
    ]
  }
}

スクリプトは次のとおりです。標準入力のJSONを読むために jq を使う前提の一例です。

#!/usr/bin/env bash
input=$(cat)
 
# すでにStop hookの結果で続行中なら、無限ループを避けて通す
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
 
if ! out=$(dotnet test tests/Api.IntegrationTests --nologo 2>&1); then
  echo "$out" | tail -40 >&2
  exit 2
fi

Stop hookの入力には stop_hook_active が含まれ、hookによる続行中は true になります。解消しない条件でブロックし続けないよう、この値を確認するのが推奨されています。上のスクリプトは、2回目の停止では通す設計です。テストが直らないまま止まる場合は、出力の末尾40行を見て手で介入します。

テストの失敗行をstderrに流すのは、Claudeに読ませるためです。成功時は何も出力せず、終了コード0で静かに終わります。

テストの寿命とデータの持ち方

統合テストでは、テスト間のデータ共有が失敗の原因になりがちです。サンプルのシードは、3件のメッセージを投入する初期化と、全件を消して入れ直す再初期化の2段に分かれています。削除系のテストが先に走ると、別のテストが削除対象を見つけられないためです。

Claudeに削除や更新のテストを書かせるときは、「そのテストの中で必要なデータを自分で用意する」と指示します。サンプルの削除テストも、WithWebHostBuilder の中でDBを再投入してからフォームを送信しています。

オブジェクトの後始末もファクトリが担います。xUnitでは IClassFixture の実装が終わると、xUnitが WebApplicationFactory を破棄し、TestServer と HttpClient も一緒に解放されます。自分で作った破棄が必要なオブジェクトは、フィクスチャ側で破棄します。MSTestでは ClassCleanup、NUnitでは TearDown が同じ役割です。

許可ルールで実行範囲を絞る

dotnet test のたびに確認が出ると、検証ループが止まります。許可ルールは Tool(specifier) の形式で、* を途中や末尾に置けます。ドキュメントの例では、Bash(npm run *) のような許可と Bash(git push *) のような拒否を組み合わせています。拒否は許可より優先され、許可ルールで拒否の例外は作れません。

{
  "permissions": {
    "allow": [
      "Bash(dotnet build *)",
      "Bash(dotnet test *)"
    ],
    "deny": [
      "Bash(git push *)"
    ]
  }
}

ただし、Bashのルールは書き方の違うコマンドを拾いません。ドキュメントは git -C . push が git push * に当たらない例を挙げています。拒否ルールは「防ぎたい操作の別の書き方もある」前提で、重要なものは他の手段も併用します。

つまずきやすい点

症状原因直し方
テストが Program を見つけられない原因暗黙の Program クラスが内部型直し方public partial class Program { } を足すか、内部型を公開する
差し替えたはずのDBが本番設定のまま原因DbContextの登録が二重になっている直し方既存の登録を services.Remove で外してから登録する
POSTだけ失敗する原因antiforgeryの検証に通っていない直し方トークンとCookieを取得してから送る(Razor Pages向け)
TempDataのCookieが消える原因GDPR同意ポリシー有効時、必須でないCookieは引き継がれない直し方テストでは該当Cookieを必須として扱う
ファイルを相対パスで読むテストが落ちる原因シャドウコピーでテストが別ディレクトリで動く直し方xunit.runner.json で "shadowCopy": false を設定する

antiforgeryの扱いは、サンプルがAngleSharpでHTMLを解析してトークンを取得しています。ただしAngleSharpは説明用のサードパーティライブラリで、統合テストの必須要件ではありません。JSON APIのコントローラーにはantiforgeryを使わない構成も多いので、必要なときだけ適用します。

コンテンツルートの推定にも注意が要ります。WebApplicationFactory はテストアセンブリの WebApplicationFactoryContentRootAttribute を探し、見つからなければ .sln を探してSUTのアセンブリ名を足したパスを使います。ソリューションファイルがないリポジトリで試すと、ビューや静的ファイルが見つからなくなるので、Claudeに新しいプロジェクト構成を作らせるときは .sln の有無を確認させます。

まとめ

Claudeに任せる範囲は、エンドポイントの実装とそのテスト1本までです。WebApplicationFactory が本番と同じパイプラインをプロセス内で動かすので、DIとルーティングのずれは dotnet test の失敗として表に出ます。CLAUDE.mdで置き場所と作法を固定し、Stop hookで実行を強制し、許可ルールで dotnet test を止めずに通す。この3つがそろえば、検証の取りこぼしはかなり減ります。

同じ「テストを完了条件にする」型は、NestJSのe2eテストやAxumのRust API、Quarkusのdevモードでも使えます。フレームワークが変わっても、検証コマンドを1本決めて自動で回す構造は変わりません。

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