CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-04-12上級

Claude API アプリケーションのテスト戦略 — AI出力の品質をユニットテスト・統合テスト・E2Eテストで保証する

Claude APIを組み込んだアプリケーションで「AIの出力が変わってテストが壊れる」問題を根本解決する。モック・セマンティックアサーション・スナップショットテストを組み合わせた実践的テスト設計パターン。

claude-api81testing5vitest2ci-cd8quality-assurance3typescript11

プレミアム記事

Claude API を本番プロダクトに組み込んだ瞬間、開発者は従来のソフトウェアテストでは経験しない壁にぶつかる。同じプロンプトを送っても返ってくる文章が毎回微妙に異なるのです。「昨日通っていたテストが今日は落ちている」——この現象に対して expect(response).toBe(exactString) で立ち向かうのは、風に向かって定規を当てるようなものです。

問題の本質は「AI の出力は確率的である」という一点に集約されます。だが、確率的ですからテストできないわけではありません。テスト対象を「出力の文字列そのもの」から「出力が満たすべき性質」にシフトすれば、堅牢なテストスイートは構築できます。

4つの AI 搭載サイトを運用するなかで固まってきた、Claude API アプリケーション固有のテスト設計パターンをまとめます。ユニットテストから E2E テストまで、各レイヤーで「何を」「どうやって」テストするのかを、Vitest + TypeScript の動作するコード例とともに示していきます。

AI アプリケーションのテストが従来のテストと根本的に違う3つの点

従来のソフトウェアテストでは、入力と出力の関係が決定的(deterministic)です。add(2, 3) は常に 5 を返す。だが Claude API を使ったアプリケーションでは、3つの不確定要素が加わる。

1. 出力の非決定性: 同一のプロンプトでも、temperature が 0 でない限り出力は毎回異なります。temperature: 0 に設定しても、モデルのアップデートで出力が変わることがあります。

2. 出力形式の揺れ: JSON を返すよう指示しても、余分な説明文が付加されたり、キー名が微妙に変わることがあります。Structured Output(tool_use)を使えばスキーマレベルでは安定するが、値の内容は依然として揺れます。

3. 品質の主観性: 「良い要約」「適切な回答」の判定は、従来の assertEquals では表現できません。人間が読んで「これは正しい」と判断する基準をコードに落とし込む必要があります。

これら3つの課題に対して、私は以下のテストピラミッドを採用しています。

  • ユニットテスト(70%): API をモックし、アプリケーションロジックの正しさを高速に検証する
  • セマンティックテスト(20%): AI 出力の「意味」をプログラム的に評価する
  • 統合テスト / E2Eテスト(10%): 実際の API を呼び出し、エンドツーエンドの動作を検証する

この比率が重要です。統合テストを増やしすぎると API コストが膨らみ、CI の実行時間も長くなります。逆にユニットテストだけではプロンプトの品質劣化を検出できません。

Claude API のモック戦略 — 3つのアプローチと実装

ユニットテストの基盤となるのが API モックです。Claude API のモックには3つのアプローチがあり、テスト対象に応じて使い分ける。

アプローチ1: SDK レベルのモック(推奨)

Anthropic SDK のクライアントをモックする方法。最もシンプルで、大半のユニットテストはこれで十分です。

// src/services/summarizer.ts
import Anthropic from "@anthropic-ai/sdk";
 
export class ArticleSummarizer {
  private client: Anthropic;
 
  constructor(client?: Anthropic) {
    // DI パターンでテスト時にモックを注入可能にする
    this.client = client ?? new Anthropic();
  }
 
  async summarize(article: string): Promise<{
    summary: string;
    keyPoints: string[];
    readingTime: number;
  }> {
    const response = await this.client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      messages: [
        {
          role: "user",
          content: `以下の記事を要約してください。JSON形式で返してください。
          {"summary": "200文字以内の要約", "keyPoints": ["要点1", "要点2", "要点3"], "readingTime": 推定読了時間(分)}
          
          記事: ${article}`,
        },
      ],
    });
 
    const text =
      response.content[0].type === "text" ? response.content[0].text : "";
 
    try {
      return JSON.parse(text);
    } catch {
      throw new Error(
        `AI応答のJSON解析に失敗: ${text.substring(0, 100)}...`
      );
    }
  }
}
// src/services/__tests__/summarizer.test.ts
import { describe, it, expect, vi } from "vitest";
import { ArticleSummarizer } from "../summarizer";
 
// モッククライアントを作成するファクトリ関数
function createMockClient(responseText: string) {
  return {
    messages: {
      create: vi.fn().mockResolvedValue({
        content: [{ type: "text", text: responseText }],
        usage: { input_tokens: 100, output_tokens: 50 },
        stop_reason: "end_turn",
      }),
    },
  } as any;
}
 
describe("ArticleSummarizer", () => {
  it("正常なJSON応答をパースできる", async () => {
    const mockResponse = JSON.stringify({
      summary: "AIテストに関する記事の要約です",
      keyPoints: ["ポイント1", "ポイント2", "ポイント3"],
      readingTime: 5,
    });
 
    const client = createMockClient(mockResponse);
    const summarizer = new ArticleSummarizer(client);
    const result = await summarizer.summarize("テスト記事の本文...");
 
    // 構造の検証(値の完全一致ではなく型と制約を検証)
    expect(result).toHaveProperty("summary");
    expect(result.summary.length).toBeLessThanOrEqual(200);
    expect(result.keyPoints).toHaveLength(3);
    expect(result.readingTime).toBeGreaterThan(0);
 
    // API呼び出しパラメータの検証
    expect(client.messages.create).toHaveBeenCalledWith(
      expect.objectContaining({
        model: "claude-sonnet-4-6",
        max_tokens: 1024,
      })
    );
  });
 
  it("不正なJSON応答時にエラーをスローする", async () => {
    const client = createMockClient("これはJSONではありません");
    const summarizer = new ArticleSummarizer(client);
 
    await expect(summarizer.summarize("テスト記事")).rejects.toThrow(
      "AI応答のJSON解析に失敗"
    );
  });
 
  it("空の応答を適切にハンドリングする", async () => {
    const client = {
      messages: {
        create: vi.fn().mockResolvedValue({
          content: [{ type: "image", source: {} }], // textブロックがない
          usage: { input_tokens: 10, output_tokens: 0 },
          stop_reason: "end_turn",
        }),
      },
    } as any;
 
    const summarizer = new ArticleSummarizer(client);
    await expect(summarizer.summarize("テスト")).rejects.toThrow();
  });
});

ここで重要なのは、テストが検証しているのはアプリケーションロジックであって、AI の出力品質ではないという点です。JSON パースの正常系・異常系、エラーハンドリング、API 呼び出しパラメータの正しさ——これらは決定的にテストできます。

アプローチ2: HTTP レベルのインターセプト(MSW)

API のリクエスト / レスポンスの形式まで含めてテストしたい場合、Mock Service Worker(MSW)を使います。

// test/mocks/handlers.ts
import { http, HttpResponse } from "msw";
 
export const claudeHandlers = [
  http.post("https://api.anthropic.com/v1/messages", async ({ request }) => {
    const body = (await request.json()) as any;
    const userMessage = body.messages?.[0]?.content || "";
 
    // プロンプトの内容に応じてレスポンスを分岐
    if (userMessage.includes("要約")) {
      return HttpResponse.json({
        id: "msg_test_001",
        type: "message",
        role: "assistant",
        content: [
          {
            type: "text",
            text: JSON.stringify({
              summary: "テスト要約",
              keyPoints: ["要点A", "要点B", "要点C"],
              readingTime: 3,
            }),
          },
        ],
        model: body.model,
        stop_reason: "end_turn",
        usage: { input_tokens: 150, output_tokens: 80 },
      });
    }
 
    // デフォルトレスポンス
    return HttpResponse.json({
      id: "msg_test_default",
      type: "message",
      role: "assistant",
      content: [{ type: "text", text: "デフォルトの応答です。" }],
      model: body.model,
      stop_reason: "end_turn",
      usage: { input_tokens: 50, output_tokens: 20 },
    });
  }),
];

MSW を使うメリットは、リクエストヘッダー(x-api-keyanthropic-version)の検証や、ストリーミングレスポンスのシミュレーションが可能な点です。ただし、セットアップのコストが SDK モックより高いため、認証やネットワーク層のテストに絞って使うのがよい。

アプローチ3: レスポンスフィクスチャ(スナップショット的運用)

実際の API レスポンスをファイルに保存し、テストで再利用する方法。初回は実 API を叩いてレスポンスを記録し、以降はそのファイルを読み込む。

// test/fixtures/record.ts — フィクスチャ記録スクリプト
import Anthropic from "@anthropic-ai/sdk";
import { writeFileSync, existsSync, mkdirSync } from "fs";
import { createHash } from "crypto";
 
const client = new Anthropic();
 
export async function recordFixture(
  name: string,
  params: Anthropic.MessageCreateParams
): Promise<void> {
  const dir = `test/fixtures/responses`;
  if (\!existsSync(dir)) mkdirSync(dir, { recursive: true });
 
  const response = await client.messages.create(params);
  const fixture = {
    params: { ...params, messages: "[REDACTED]" }, // プロンプトは保存しない
    response,
    recordedAt: new Date().toISOString(),
    modelVersion: response.model,
  };
 
  writeFileSync(
    `${dir}/${name}.json`,
    JSON.stringify(fixture, null, 2)
  );
  console.log(`✅ フィクスチャ記録: ${name} (${response.usage.output_tokens} tokens)`);
}

フィクスチャ方式の注意点として、モデルバージョンが変わったらフィクスチャも更新する必要があるrecordedAtmodelVersion をメタデータに含めておけば、古いフィクスチャの検出が容易になります。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
Claude APIを使ったアプリで「AIの出力が変わってテストが壊れる」問題に悩んでいた人が、確実に動くテスト設計パターンを手に入れられる
モック・スナップショット・セマンティックアサーション戦略を組み合わせて、CI/CDパイプラインにAIテストを組み込めるようになる
テスト用のAPI呼び出しコストを月額数ドル以下に抑えながら、プロンプト変更のリグレッションを確実に検出する仕組みを構築できる
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

API & SDK2026-04-26
Claude API のReplay駆動テスト — 本番応答を録画して回帰テストを成立させる設計パターン
Claude APIの非決定的な応答を録画・再生してE2Eテストを安定運用するReplay駆動テストの本番設計。カセット形式・差分検証・CI統合まで実装パターンで解説します。
API & SDK2026-03-27
Claude API LLM評価パイプライン構築ガイド — Claude-as-Judge・プロンプトA/Bテスト・品質スコアリングの実装パターン
Claude API を使った LLM アプリケーション評価パイプラインの設計と実装。Claude-as-Judge、プロンプト A/B テスト、品質スコアリング、回帰テストの本番実装パターンを扱います。
API & SDK2026-06-19
Claude × Playwright のブラウザエージェントが「成功したつもり」で失敗するとき — 操作を検証し UI の変化に先回りする実装メモ
Claude API と Playwright のブラウザエージェントは、視覚で判断する分だけ「やったつもりで失敗する」誤成功が起きます。自己申告を信じず操作を事後検証する設計と、UI 変化に先回りで気づくドリフト検知の実装をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →