> ## Documentation Index
> Fetch the complete documentation index at: https://lib.findy.co.jp/llms.txt
> Use this file to discover all available pages before exploring further.

# テストコード

> カバレッジの数字ではなく、通るべき時に通りコケるべき時にコケる、システムを守るテストコードを書きます。

## 概要

テストコードは、システムが意図どおりに振る舞うことを検証するコードであり、継続的な変更を可能にする安全網です。

テストコードで重要なのは、**システムを守る**ことです。システムを守るテストとは、挙動が正しいときに通り、挙動が壊れたときにコケるテストのことです。その価値はテストコードが全て通ることではなく、本番環境にリリースされる前に意図しない挙動を防ぐことにあります。

本ページでは、そうしたテストを支える原則と、テストを保護的にする書き方を解説します。

## 原則

### テストの目的はソフトウェアの成長を持続可能にすること

自動テストの目的は、ソフトウェアの成長を持続可能にすることです。変更の影響を信頼できる結果として短時間で確認できる状態を保つことで、チームは自信を持ってシステムを変更し続けられます。意図しない挙動の検知はそのための手段であり、それ自体が最終目的ではありません。

したがって、質とスピードはトレードオフではありません。テストを省いて得られる速さは一時的なもので、その代償は意図しない挙動の混入・手動での再確認・既存コードへ手を入れることへの恐れとして、すぐに返ってきます。テストを書く時間が無いのではなく、テストを書かないから時間が無いのです。

リファクタリングや段階的な改善には、変更後も既存の挙動が保たれているという確信が必要であり、テストコードがそれを与えます。

生成AIツールが変更を提案する時代には、この点はさらに重要になります。提案された変更を受け入れてよいかどうかに最終的に答えるのはテストコードであり、テストコードの品質がどれだけの作業を委任できるかを決めます。

### 通ることもコケることも信頼できる状態を保つ

テストコードがシステムを守れるのは、その結果を両方向で信頼できる間だけです。テストコードが信頼を損なう失敗には2つの種類があります。

<CardGroup cols={2}>
  <Card title="偽陰性 — 壊れているのに通る" icon="eye-slash">
    挙動が壊れているのにテストが通ることです。断片的な検証や検証対象のモックが原因で起こり、存在しない安全が報告されて、意図しない挙動が本番環境にリリースされてしまいます。
  </Card>

  <Card title="偽陽性 — 正しいのにコケる" icon="triangle-exclamation">
    挙動は正しいのにテストがコケることです。実装の詳細や不安定なタイミングへの依存が原因で起こり、頻発すると失敗が無視されるようになって、本当に挙動が壊れたときまで見逃されます。
  </Card>
</CardGroup>

検証を強化して偽陰性を減らし、実装の内部ではなく観察可能な挙動を検証して偽陽性を減らします。両方を低く保つことで、テストコードは信頼を獲得します。本ページの後半の検証パターンは、主に偽陰性を防ぐためのものです。

### カバレッジは目的ではなく結果

カバレッジは、テストコードがどの行を実行したかを測るものであり、正しい挙動を検証しているかを測るものではありません。ある行は、それが正しく動くという検証なしに実行できてしまうため、高いカバレッジと「ほとんど何も守らないテスト」は両立してしまいます。

確実な順序は、まず**絶対に壊れてはいけない挙動**を決め、それが壊れたら検知できるテストケースを追加することです。カバレッジはその結果として上がります。

### テストは実装と同じプルリクエストで書く

挙動の変更と、それを守るテストは同じプルリクエストに含めます。テストを後続の対応に先送りすると、その挙動が守られていない期間が生まれ、実際には後続の対応は新しい仕事と競合して後回しになりがちです。

テストコードのレビューは実装コードと同等以上に厳密に行います。弱い検証を今日承認することは、将来の意図しない挙動を見逃すことと同じです。

変更をレビューしやすい単位に保つ方法は[プルリクエスト](/ja/development/pull-request)を参照してください。

## テストを保護的にする書き方

次のパターンは、テストが一見問題なさそうでも、本来捕まえるはずのバグを捕まえられないケース、すなわち偽陰性の原因です。いずれも、重要な壊れ方に対して確実にコケるところまでテストを強化します。

同じパターンは、生成AIツールが出力するテストコードに繰り返し現れる弱点でもあります。生成されたテストは保護ではなく「テストが通ること」に最適化されがちなので、マージ前に人が書いたテストと同じ厳しさで、これらのパターンに照らして見直します。

### 1. 実行されたかではなく、実行状態を検証する

コールバックが呼ばれたことだけを検証すると、3つのバグを隠します。1回でよいハンドラが2回発火するケース、誤った引数（別のユーザーのデータなど）で呼ばれるケース、そして本来は決して走らないはずのエラー経路のコールバックが走るケースです。回数と引数まで固定し、呼ばれないはずの処理が一度も呼ばれていないことを検証します。

<CodeGroup>
  ```javascript Jest theme={null}
  // ❌ 呼ばれたことしか検証していない
  // （2回の発火・誤った引数・onErrorの発火がすべて通る）
  expect(onSuccess).toHaveBeenCalled();

  // ✅ 回数・引数・呼ばれない経路を固定する
  expect(onSuccess).toHaveBeenCalledTimes(1);
  expect(onSuccess).toHaveBeenCalledWith({ userId: 123 });
  expect(onError).not.toHaveBeenCalled();
  ```

  ```ruby RSpec theme={null}
  # ❌ 呼ばれたことしか検証していない
  # （2回の呼び出し・誤った引数・on_errorの呼び出しがすべて通る）
  expect(on_success).to have_received(:call)

  # ✅ 回数・引数・呼ばれない経路を固定する
  expect(on_success).to have_received(:call).with(user_id: 123).once
  expect(on_error).not_to have_received(:call)
  ```
</CodeGroup>

### 2. 一部ではなく、期待値の全体を検証する

結果のうち1つのフィールドだけを確認すると、それ以外のフィールドはすべて無防備になります。検証していないフィールドの欠落・改名・破損は、静かに通過してしまいます。構造全体をリテラルの期待値と比較し、結果の形や内容への意図しない変更がテストを確実に失敗させるようにします。

<CodeGroup>
  ```javascript Jest theme={null}
  // ❌ フィールド単体しか検証していない
  expect(result.id).toBe(1);
  expect(result.name).toBe("Alice");
  expect(result.role).toBe("member");

  // ✅ 構造全体を固定する（意図しない変更はすべてコケる）
  expect(result).toStrictEqual({ id: 1, name: "Alice", role: "member" });

  // ✅ フィールド単体に加えて、キーもチェックする
  // （キーの過不足＝フィールドの欠落や意図しない追加もコケる）
  expect(Object.keys(result).sort()).toEqual(["id", "name", "role"]);
  expect(result.id).toBe(1);
  expect(result.name).toBe("Alice");
  expect(result.role).toBe("member");
  ```

  ```ruby RSpec theme={null}
  # ❌ フィールド単体しか検証していない
  expect(result[:id]).to eq(1)
  expect(result[:name]).to eq("Alice")
  expect(result[:role]).to eq("member")

  # ✅ 構造全体を固定する（意図しない変更はすべてコケる）
  expect(result).to eq({ id: 1, name: "Alice", role: "member" })

  # ✅ フィールド単体に加えて、キーもチェックする
  # （キーの過不足＝フィールドの欠落や意図しない追加もコケる）
  expect(result.keys).to contain_exactly(:id, :name, :role)
  expect(result[:id]).to eq(1)
  expect(result[:name]).to eq("Alice")
  expect(result[:role]).to eq("member")
  ```
</CodeGroup>

生成されたテストでは特に、現在の挙動が通るのに必要な最小限の検証しか書かれないことがよくあります。今日はテストが通っても、検証されていない部分の挙動を壊す将来の拡張は保証されません。

<CodeGroup>
  ```javascript Jest theme={null}
  // ❌ 件数しか検証していない（中身が別物でも2件なら通る）
  expect(result.items.length).toBe(2);

  // ✅ 期待値の全体を固定する（商品・価格・合計のどれが壊れてもコケる）
  expect(result).toStrictEqual({
    items: [
      { id: 1, name: "Coffee", price: 500 },
      { id: 2, name: "Beans", price: 1200 },
    ],
    total: 1700,
  });
  ```

  ```ruby RSpec theme={null}
  # ❌ 件数しか検証していない（中身が別物でも2件なら通る）
  expect(result[:items].length).to eq(2)

  # ✅ 期待値の全体を固定する（商品・価格・合計のどれが壊れてもコケる）
  expect(result).to eq({
    items: [
      { id: 1, name: "Coffee", price: 500 },
      { id: 2, name: "Beans", price: 1200 }
    ],
    total: 1700
  })
  ```
</CodeGroup>

ゆるいマッチャも同じように検証を弱めます。`toBeTruthy`や`be_truthy`は空でない文字列・数値など、真と評価される任意の値でも通るため、リファクタリングがbooleanの返り値を別の型にこっそり変えても通り続けます。正確な値と型を検証します。

<CodeGroup>
  ```javascript Jest theme={null}
  // ❌ trueでなくても通ってしまう（"yes"や1でも通る）
  expect(active).toBeTruthy();

  // ✅ 正確な値と型を検証する
  expect(active).toBe(true);
  expect(count).toBe(0);
  ```

  ```ruby RSpec theme={null}
  # ❌ trueでなくても通ってしまう（"yes"や1でも通る）
  expect(active?).to be_truthy

  # ✅ 正確な値と型を検証する
  expect(active?).to be true
  expect(count).to eq 0
  ```
</CodeGroup>

### 3. 検証対象のロジックではなく、外部境界をモックする

「テストが通ること」が目的になると、生成AIツールは検証対象のロジックそのものをモックしたり、期待値を実装が現在返す値に合わせて調整したりすることがあります。実装自体を答え合わせの正解として扱っているため、実装が誤っていてもテストは通ってしまいます。

モックの本来の役割はその逆で、現在時刻やネットワーク応答のような外部境界を固定し、出力を決定的にすることです。たとえば有効期限の日時を含むメール本文のように現在時刻に依存する出力は、時刻を固定しない限り断片的な検証しかできません。時刻をモックで固定すれば、パターン2のとおり期待値の全体を検証できます。

どのケースでモックをどう使うかをプロジェクトのカスタムインストラクションに記述し、期待値が実装の出力ではなく仕様に由来していることをレビューで確認します。

<CodeGroup>
  ```javascript Jest theme={null}
  // テスト対象の実装: 有効期限（現在時刻の7日後）を含むメール本文を組み立てる
  const mailService = {
    createInviteMailBody(toEmail, inviteLink) {
      const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
      const expiryDate = expiresAt.toISOString().slice(0, 10); // 例: "2026-07-08"
      return `${toEmail}様\n${inviteLink}\nこのリンクの有効期限は ${expiryDate} です`;
    },
  };

  const expectedBody = `${toEmail}様\n${inviteLink}\nこのリンクの有効期限は 2026-07-08 です`;

  // ❌ テスト対象そのものをモックしている
  // （モックの返り値を検証するだけで、実装が壊れていても通る）
  jest.spyOn(mailService, "createInviteMailBody").mockReturnValue(expectedBody);
  expect(mailService.createInviteMailBody(toEmail, inviteLink)).toBe(expectedBody);

  // ✅ 外部境界（現在時刻）だけをモックし、本物のロジックを動かす
  // （有効期限が2026-07-08に確定し、実装のバグはコケる）
  jest.useFakeTimers().setSystemTime(new Date("2026-07-01T00:00:00Z"));
  expect(mailService.createInviteMailBody(toEmail, inviteLink)).toBe(expectedBody);
  ```

  ```ruby RSpec theme={null}
  # テスト対象の実装: 有効期限（現在時刻の7日後）を含むメール本文を組み立てる
  class MailService
    def create_invite_mail_body(to_email, invite_link)
      expiry_date = (Time.zone.now + 7.days).strftime("%Y-%m-%d") # 例: "2026-07-08"
      "#{to_email}様\n#{invite_link}\nこのリンクの有効期限は #{expiry_date} です"
    end
  end

  expected_body = "#{to_email}様\n#{invite_link}\nこのリンクの有効期限は 2026-07-08 です"

  # ❌ テスト対象そのものをモックしている
  # （モックの返り値を検証するだけで、実装が壊れていても通る）
  allow(mail_service).to receive(:create_invite_mail_body).and_return(expected_body)
  expect(mail_service.create_invite_mail_body(to_email, invite_link)).to eq(expected_body)

  # ✅ 外部境界（現在時刻）だけをモックし、本物のロジックを動かす
  # （有効期限が2026-07-08に確定し、実装のバグはコケる）
  travel_to Time.zone.local(2026, 7, 1) do
    expect(mail_service.create_invite_mail_body(to_email, invite_link)).to eq(expected_body)
  end
  ```
</CodeGroup>

## 関連ページ

<CardGroup cols={2}>
  <Card title="プルリクエスト" icon="code-pull-request" href="/ja/development/pull-request">
    テストは、それが守る変更と同じレビュー単位で提出します。
  </Card>

  <Card title="Vibe Coding" icon="wand-magic-sparkles" href="/ja/ai/vibe-coding">
    信頼できるスイートの上で、実装を生成AIツールに委任します。
  </Card>
</CardGroup>
