unit-testing について
Jest、Vitest、pytestを使う開発者向けのユニットテストスキルです。テストダブル、実効性のあるカバレッジゲート、スナップショット、フェイクタイマー、ミューテーションテストを扱います。
主な機能
- 01Jest、Vitest、pytestのパターン
- 02スタブ、スパイ、モック、フェイクの指針
- 03カバレッジ閾値の設定
- 04スナップショットとフェイクタイマー
- 05動作検証のためのミューテーションテスト
活用シーン
- 01CIのカバレッジゲート設定
- 02外部境界で使うテストダブルの選択
- 03時間依存動作の決定的なテスト
- 04ロジック変更を見逃すテストの特定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ petrkindlmann/ qa-skills --skill unit-testingインストール後、Claude Code で/unit-testingを入力して読み込まれたか確認します。
同梱ファイルreferences/patterns.md
詳細
確認事項
まず .agents/qa-project-context.md を確認してください。存在する場合はその内容を使い、そこで回答済みの項目は省略します。
- フレームワーク: Jest、Vitest、pytest のどれですか?
package.jsonまたはpyproject.tomlを確認してください。テストランナーによって設定キーとモック API が決まります。 - カバレッジツール: すでに設定されていますか?
jest.config.*、vitest.config.*、.nycrc、[tool.coverage]を探してください。ゲートを追加するか、調整するだけでよいかが分かります。 - モック戦略: 手動モック、自動モック、依存性注入のどれですか?
__mocks__/ディレクトリや DI コンテナを確認してください。どのテストダブルを使うかが決まります。 - 規約: ソースの隣に配置する方式(
*.test.ts)ですか、それとも__tests__/tests/ツリーですか?既存の方式に合わせ、3 つ目の配置場所を導入しないでください。
基本原則
1. 実装ではなく動作をテストします。 コードの「どのように」ではなく、「何を」するかを検証します。内部のリファクタリングでテストが壊れてはいけません。
// Bad — implementation detail // Good — observable behavior
expect(svc._cache.size).toBe(3); expect(svc.getUser("abc")).toEqual({ id: "abc", name: "Alice" });
2. 高速、分離、決定論的にします。 ネットワーク/ディスク/DB は使いません。共有可変状態も使いません。制御されていない Date.now() や Math.random() は使わず、フェイクタイマーとシード値で固定します。
3. 準備・実行・検証の順にします。 テストごとに明確な構成を 1 つにします。
it("should apply discount for orders over $100", () => {
// Arrange
const order = createOrder({ subtotal: 150 });
const svc = new DiscountService(0.1);
// Act
const result = svc.apply(order);
// Assert
expect(result.total).toBe(135);
});
4. 1 つのテストにつき、アサーションの概念は 1 つにします。 同じ概念を検証する場合は、expect の呼び出しが複数あってもかまいません。
5. 説明的な名前を付けます。 "test calculateTotal" ではなく、"should [behavior] when [condition]" のようにします。
フレームワーク別のパターン
各テストランナーのセットアップ/ティアダウン、モック、スパイ、タイマー、ソース内テスト、モノレポの詳しい例は references/patterns.md にあります。以下では、現在の状況と利用する機能を紹介します。コードはリファレンスからコピーしてください。
Jest
現在のバージョンは Jest 30.x(30.4.2、2026 年 5 月)です。Jest 30 では --collect-tests、jest.config.mts のサポート、Temporal 対応のフェイクタイマー、clearMocksOnScope が追加されました。テスト対象のコードで Temporal API やタイムゾーンのロジックを使う場合、Jest 30 の Temporal 対応フェイクタイマーによって、不安定なセットアップの一種を取り除けます。
使う機能:モジュール境界には jest.mock()(部分モックには jest.requireActual)、実際のメソッドをラップするには jest.spyOn()、型付きモックには jest.Mocked<T>、時間を扱うには jest.useFakeTimers() を使います。references/patterns.md の「Jest」を参照してください。
Vitest
Jest と同じ API を備え、Vite にネイティブ対応しています。安定版は Vitest 4.1.x(2026 年 6 月)です。5.0.0-beta がリリースされています(beta.3、2026 年 5 月)。Vitest 4 では coverage.changed(変更ファイルのみのカバレッジ)、mockThrow/mockThrowOnce、安定版のブラウザモードが追加されました。Vitest 5 ベータ版では sequential オプションが削除され、Node 22/Vite 6.4 が必要です。安定版になるまで導入は待ってください。モックには vi.mock/vi.spyOn を使います。特筆すべき機能は、ソース内テスト(import.meta.vitest)と、コンポーネントのレンダリング向けのブラウザモードです。references/patterns.md の「Vitest」を参照してください。
pytest
フィクスチャと conftest.py(ティアダウンには yield を使用)、データ駆動型のケースには @pytest.mark.parametrize、環境変数や属性の置き換えには monkeypatch を使います。setUp/tearDown メソッドよりもフィクスチャを優先してください。フィクスチャは組み合わせやすく、テストごとに分離できるためです。references/patterns.md の「pytest」を参照してください。
Bun / Deno
ランタイムがすでに Bun または Deno の場合は、bun test(Jest 互換で追加設定不要)と deno test(ネイティブ TS、権限フラグ)が妥当な既定の選択肢です。より充実したプラグインエコシステムを備える Node プロジェクトでは、Vitest/Jest を優先してください。
モックの分類
目的を果たす最もシンプルなテストダブルを選びます。ほとんどの場合、それはスタブです。
| テストダブル | 役割 | 使用する場面 |
|---|---|---|
| スタブ | あらかじめ用意したデータを返し、検証はしません | 依存先の戻り値を制御する |
| スパイ | 実際の実装をラップし、呼び出しを記録します | 動作を変えずに呼び出しを検証する |
| モック | 実装を置き換え、呼び出しを記録します | 戻り値を制御し、やり取りも検証する |
| フェイク | 簡易的な動作する実装(インメモリ DB など)です | 複雑な状態を持つ依存先に使用する |
経験則: モックよりスタブを優先し、状態を持つ依存先にはフェイクを使用します。
ユニットテストで実際の外部 API を呼び出してはいけません。モックにするのは
外部境界(ネットワーク、ファイルシステム、DB、時間)だけにします。高速で
決定的な内部の協力オブジェクトは実際に動かしてください。そうしないと、統合部分が
壊れているのにテストスイートが成功することになります。コードでの 4 種類の
テストダブルについては、references/patterns.md § テストダブルを参照してください。
カバレッジ
設定
Jest — 閾値のキーは coverageThreshold(単数形)です。複数形の
coverageThresholds は Jest のキーではありません。Jest はこれを警告なしに無視するため、
ゲートは適用されず、カバレッジが 30% でも CI は成功します。これは設定ミスとして最もよくあります。
// jest.config.js
module.exports = {
coverageProvider: "v8",
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.{d,test,stories}.ts", "!src/**/index.ts"],
coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } },
};
Vitest — vitest.config.ts で provider: "v8" とともに test.coverage.thresholds を
設定します(設定全体については references/patterns.md § Vitest を参照してください)。
pytest:
# pyproject.toml
[tool.coverage.run]
source = ["src"]
omit = ["src/**/test_*.py", "src/**/conftest.py"]
[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"]
カバレッジの種類とゲートに使うもの
| 種類 | 測定する内容 | 見落とす点 |
|---|---|---|
| ブランチ | if/else のすべての経路が実行されたか | 値の組み合わせは見落とします |
| 行 | 各行が実行されたか | 1 行内の未テストの分岐は見落とします |
| ステートメント | 各ステートメントが実行されたか | 行カバレッジと同様です |
| 関数 | 各関数が呼び出されたか | 正しさについては何も分かりません |
優先順位: ブランチ > 行 > ステートメント > 関数。見栄えのための目標ではなく、 行カバレッジ 80% をベースラインのゲートとして使い、ブランチカバレッジをより重視します。 ビジネスロジック、変換処理、エラー経路、エッジケースのカバレッジに注力し、 生成コード、型定義、バレルエクスポート、単純な getter、フレームワークの定型コードは対象外にします。
カバレッジがない行のうち、どれが重要かを判断し、ギャップ分析を行うには、 このスキルではなく
coverage-analysisを使用してください。
CI ゲート
Jest と Vitest は閾値を下回ると 0 以外の終了コードを返します。この終了コードが ゲートになります。pytest ではフラグを明示的に指定する必要があります。
- run: pytest --cov=src --cov-fail-under=80
ミューテーションテスト
カバレッジから分かるのは、コードが実行されたことです。ミューテーションテストから分かるのは、
テストがバグを検出できるかです。ソースコードに小さな変更(> → >=、true → false)を加え、
各ミュータントに対してテストスイートを再実行します。それでもテストスイートが成功する場合、
ミュータントは生き残ったことになります。そのロジックはテストで実行されていても、
アサーションで検証されていなかったということです。
Stryker (JS/TS)
npm i -D @stryker-mutator/core @stryker-mutator/jest-runner # or vitest-runner
// stryker.config.json (Stryker's documented default; .mjs/.mts also load)
{
"testRunner": "jest",
"coverageAnalysis": "perTest",
"mutate": ["src/**/*.ts", "!src/**/*.test.ts"],
"thresholds": { "high": 80, "low": 60, "break": 50 },
"reporters": ["html", "clear-text", "progress"]
}
Stryker 自体のデフォルト値は { high: 80, low: 60, break: null } です。break: null は
失敗終了しないことを意味します。CI で低いスコアを失敗にするには、break(例:50)を設定します。
実行方法:npx stryker run。
mutmut (Python) — mutmut 3.x
mutmut 3 では以前の CLI 機能が削除されました。[mutmut] ブロックでパスを設定して実行し、
TUI で生き残ったミュータントを確認します。
# setup.cfg (or a [tool.mutmut] table in pyproject.toml)
[mutmut]
paths_to_mutate=src/
pip install mutmut # 3.5.x
mutmut run # paths come from config, not a flag
mutmut browse # interactive TUI: inspect and retest survivors
mutmut apply <mutant_id> # write a survivor to disk to see what it changed
避けること:
mutmut run --paths-to-mutate=src/、mutmut results、mutmut show 42— これらは mutmut <3 の機能です。--paths-to-mutateフラグは削除されており(パスは[mutmut]設定ブロックに移動)、results/showはbrowse/applyに置き換えられています (mutmut 3.5.x、2026 年 6 月に確認済み)。古いコマンドを使うと、現在のインストール環境ではエラーになります。
スコアの解釈
| スコア | 意味 |
|---|---|
| 90%+ | 強力 — ほとんどのロジック変更を検出しています |
| 70–89% | まずまず — 重要な経路で生き残ったミュータントを確認してください |
| <70% | コードは実行していますが、動作を検証できていません |
ミューテーションテストは、コードベース全体ではなく重要なビジネスロジックに対して実行します (時間がかかるためです)。等価ミュータントは無視してください。これは論理的に同一のコードであり、 どのテストでも違いを判別できません。
スナップショットテスト
用途: UI コンポーネントのレンダリング出力、シリアライズされたデータ構造、CLI のフォーマットなど、正確な構造が重要で、フィールドごとのアサーションが煩雑な出力に使用します。
使用しないケース: 頻繁に変わる出力(スナップショット疲れにより、レビューが形骸化します)、大きなスナップショット(レビューできません)、実装の詳細(CSS クラス、内部 ID)、または特定の値が重要な場合に、対象を絞ったアサーションの代わりとして使用すること。
小さな出力(20 行未満)にはインラインスナップショットを、ID やタイムスタンプなどの動的なフィールドにはプロパティマッチャー(expect.any(String))を優先してください。未知のスナップショットが黙って書き込まれてコミットされるのではなく、失敗するよう、CI は必ず --ci を指定して実行してください。
コード:references/patterns.md § スナップショットテスト。
アンチパターン
プライベートメソッドのテスト — パブリック API を通じてテストしてください。プライベートメソッドをどうしても個別にテストする必要がある場合は、独自のパブリックなインターフェースを持つモジュールに切り出してください。
すべてをモックする — 外部との境界(ネットワーク、ファイルシステム、DB、時間)だけをモックしてください。すべての協調相手をモックしたテストスイートは、相互の接続が壊れていてもパスします。
複数形の coverageThresholds — Jest はこれを無視するため、ゲートは機能せず、カバレッジに関係なく CI はグリーンになります。キーは単数形の coverageThreshold です。上記のカバレッジを参照してください。
すべてのタイマーを無条件に偽装する — 許可リストを指定せずに jest.useFakeTimers() / vi.useFakeTimers() を使用すると、実際のマイクロタスクを待つコードがデッドロックする可能性があります。テストで必要なものだけを偽装してください(doNotFake / toFake)。references/patterns.md § Jest タイマーを参照してください。
await のない非同期テスト — await を忘れるとアサーションが実行されず、テストは実質的に何も検証しないままパスします。非同期テストに expect.assertions(n) / expect.hasAssertions() を追加し、アサーションが欠けている場合に失敗するようにしてください。
スナップショットの使いすぎ — 特定の値には expect(x).toBe("active") を使用し、フィールドごとのアサーションができない構造化出力に限ってスナップショットを使用してください。
説明的でない名前 — "works" を "should return empty array when no items match the filter" に置き換えてください。
共有される可変状態 — モジュールスコープではなく、beforeEach で初期化してください:
// Bad: shared mutation // Good: fresh per test
const items = []; let items: string[];
it("A", () => items.push("a")); beforeEach(() => { items = []; });
it("B", () => { it("A", () => { items.push("a"); expect(items).toHaveLength(1); });
items.push("b"); it("B", () => { items.push("b"); expect(items).toHaveLength(1); });
expect(items).toHaveLength(1); // FAILS
});
検証
テストスイートが実行されることと、カバレッジ不足時にゲートが実際に失敗することを確認してください。これは coverageThreshold の誤記によって黙って無効化される、まさにその機能です。
- テストが実行され、パスする:
npx jest(またはvitest run、pytest -q)が終了コード0で終了します。 - ゲートが機能する。 カバレッジを実行し、しきい値を下回った場合に終了コードが 0 以外になることを確認してください:
しきい値を現在のカバレッジより一時的に高く設定し(例:99)、コマンドが失敗することを確認してください。終了コードがnpx jest --coverage --ci # Jest/Vitest exit !=0 below coverageThreshold vitest run --coverage # same for Vitest pytest --cov=src --cov-fail-under=80 # pytest exits !=0 below the floor0の場合、しきい値のキーが間違っています(複数形のcoverageThresholdsである可能性が高いです)。 - CI でスナップショットが安全である: 実行時に
--ciを指定し、未知のスナップショットが書き込まれるのではなく失敗します。CI モードで実行した後、git statusに新しい*.snapが表示されないことを確認してください。
完了条件
jest.config.*(キーは単数形のcoverageThreshold)、vitest.config.*(coverage.thresholds)、またはpyproject.toml(fail_under)でカバレッジのしきい値が設定され、しきい値を下回ると終了コードが 0 以外になることを確認済み(検証の手順 2)- テストファイルがすべて、プロジェクトで選択した単一の場所(同じ場所に配置、または
__tests__/tests/)にあり、git ls-filesで場当たり的なテストパスが表示されない - 外部との境界(HTTP、DB、時間)はモックされ、内部の協調相手はモックされていない — テストファイル内で実際のネットワーク/DB クライアントが生成されていないことを
grepで確認 - プロセス境界の外にアクセスするテストがない — ネットワークを無効にし、テスト用 DB を起動せずにテストスイートがパスする
- CI ではテストコマンドを
--ci(Jest/Vitest)付きで実行し、未知のスナップショットが自動で書き込まれるのではなく、ビルドが失敗する
リファレンスファイル(references/ 内)
- patterns.md — 各フレームワークでそのまま実行できる詳しい例:Jest のセットアップ/後処理、モジュール/スパイ/タイマーのモック、非同期のガード、Vitest の設定、ソース内テスト、並行実行、ブラウザーモード、pytest のフィクスチャ/パラメーター化/monkeypatch、Bun/Deno、4 種類のテストダブル、スナップショットのファイル/インライン/プロパティマッチャー。
関連スキル
- coverage-analysis — カバレッジレポートの解釈、有意義なギャップの特定、主要なシグナルとしてのミューテーションスコアについて説明します。カバレッジを確認する場合はそちらを参照し、ここでは設定とゲートの構成を行います。
- ci-cd-integration — パイプライン内のテストステージ、並列化、キャッシュ、デプロイのゲート設定について説明します。
- ai-test-generation — AI が仕様書や PRD からテストコードを生成する場合に使用します。このスキルでは、手動でのテストの作成と構成を扱います。
- ai-qa-review — 既存のテストにおける、幻覚による API、捏造されたインポート、閉ループテストを監査します。
- shift-left-testing — これらのテストに関する、コミット前フック、IDE との統合、TDD ワークフローについて説明します。