良い単体テストとは何か — FIRST原則から契約テストまで

実行できてgreenになるテストは誰でも書けるが、そのすべてが価値を生むわけではない。FIRST原則とVladimir Khorikovの「4本柱」で単体テストの良し悪しを定義し、AAAパターンや命名規約を経て、単体の外側——Fowlerのnarrow/broad統合テスト、Consumer-Driven ContractsとPactによるサービス間の契約保証——まで、テストの境界をどう設計するかを辿る。

testingunit-testingkhorikovmockscontract-testingpact

「良いテスト」という難問

単体テストは書くこと自体は難しくない。実行できてgreenになるテストは無数に書けるが、そのすべてが価値を生むわけではない。むしろ設計の悪いテストは、リファクタリングのたびに壊れ、開発速度を落とす負債になる。「テストとはどうあるべきか」を定義しようとした2つの代表的な枠組みがある。古典的なFIRST原則と、より新しいVladimir Khorikovの 「4本柱」 である。この2つを軸に、テストの独立性・粒度・命名という実務的な設計判断から、単体テストの境界を越えた統合テスト・契約テストの世界までを辿る。

FIRST原則 — テストの実行特性

FIRSTは、良い単体テストが満たすべき5つの性質の頭文字を取った標語で、Robert C. Martinの著書『Clean Code』(2008年)の中で、Tim OttingerとJeff Langrによる定式化として紹介された。

頭文字 原則 意味
F Fast(高速) ミリ秒単位で終わる。遅いテストは実行頻度が下がり価値を失う
I Independent(独立) 他のテストの実行順序・結果に依存しない
R Repeatable(反復可能) 何度実行しても、どの環境でも同じ結果になる
S Self-Validating(自己検証) 目視でログを読まなくても、pass/failが機械的に判定される
T Timely(適時) 実装の直後、あるいはTDDなら実装の前に書く

特にIndependentとRepeatableは、実行するたびに結果が揺れる「フレーキーテスト」の直接的な予防策になる(詳しくはフレーキーテストとの戦いを参照)。テスト間でグローバル変数やDBの状態を共有すると、実行順序によって結果が変わるテストスイートが生まれる。

Khorikovの「4本柱」 — 何のためにテストは存在するか

Robert C. MartinのFIRSTが「テストの実行特性」に主眼を置くのに対し、Vladimir Khorikovは著書『Unit Testing Principles, Practices, and Patterns』(Manning、2020年)で、テストが長期的に価値を生み続けるための4つの柱を提示した。

  1. 退行に対する保護 — バグが混入したとき、そのテストが確実に検知できるか
  2. リファクタリング耐性 — 振る舞いを変えない内部実装の変更に対して、偽陽性(本当は壊れていないのに落ちる)を出さないか
  3. 迅速なフィードバック — 実行が速く、開発のリズムを妨げないか
  4. 保守性 — テストコード自体が読みやすく、書くコストが低いか

Khorikovの議論の核心は、この4つを同時に最大化することはできないという指摘にある。リファクタリング耐性と保守性はある意味で二値的な性質で、テストが実装の詳細に結合していれば壊れ、していなければ壊れない。一方、退行保護と迅速なフィードバックは連続的なトレードオフになりやすい。より広い範囲を検証すれば退行保護は上がるが実行は遅くなる。

Khorikovが特に警告するのは、モックの過剰使用がリファクタリング耐性を静かに破壊する点である。「呼び出されたか」「どの引数で呼ばれたか」を検証するテストは実装の詳細に密結合しやすく、内部実装を変えただけでテストが赤くなる。Khorikovはモックを「テスト対象と外部との境界(プロセス外依存)」にのみ使うべきだと主張する(モック自体の歴史はモックとテストダブルの発展を参照)。

テストの独立性 — 3つの水準

FIRSTのIndependentとも重なるが、独立性は次の3水準で確保する必要がある。

  • 状態の独立: あるテストが書き換えたグローバル状態・共有DBレコードが、別のテストの前提を壊さない
  • 順序の独立: テストランナーがどの順番で実行しても結果が変わらない
  • 環境の独立: ローカル・CI・開発者ごとのマシンで同じ結果になる

典型的な手法として、各テストの前後で状態を初期化する(トランザクションのロールバック等)、テストごとに新しいインメモリの依存を生成する、依存性注入(DI)でグローバルなsingletonへの依存を避ける、といった設計がある。

1テスト1振る舞い

良い単体テストは、1つのテストケースで1つの振る舞いだけを検証する。避けるべきは、無関係な複数の振る舞いを1つのテストにまとめてしまうことである。

// 悪い例: 複数の無関係な振る舞いを1テストに詰め込む
test("ユーザー登録", () => {
  const user = registerUser("alice@example.com", "password123");
  expect(user.email).toBe("alice@example.com");   // 振る舞い1
  expect(sendWelcomeEmail).toHaveBeenCalled();      // 振る舞い2
  expect(auditLog.length).toBe(1);                  // 振る舞い3
});

// 良い例: 振る舞いごとにテストを分ける
test("登録するとメールアドレスが保存される", () => { /* ... */ });
test("登録するとウェルカムメールが送信される", () => { /* ... */ });

これを守ると、テストが失敗したときにテスト名だけで「何が壊れたか」が分かる。1つのテストに多数のアサーションが並び、どれが失敗したのか実行結果を読まないと分からない状態は「アサーションルーレット」と呼ばれ、避けるべきアンチパターンとされる。

AAAパターン

テストの内部構造を整理する慣習として広く使われるのがAAAパターン(Arrange-Act-Assert)である。Bill Wakeが2001年のブログ記事で「3A」として紹介したとされ、以後xUnit系フレームワークのデファクトの記述順序として定着した。

def test_withdraw_more_than_balance_raises_error():
    # Arrange: 準備
    account = Account(balance=1000)

    # Act & Assert: 実行と検証
    with pytest.raises(InsufficientFundsError):
        account.withdraw(2000)

BDD系のツールでは同じ構造がGiven-When-Thenという語彙で表現される(BDDの登場を参照)。名前は違えど「準備・実行・検証」を分離するという発想は共通している。

何をテストしないか

単体テストの設計は「何を書かないか」の判断も含む。書く価値の低いテストは以下のようなものである。

  • 自明なコード: 単純なgetter/setter、ロジックのないコード
  • 実装の詳細: privateメソッドの内部手順、呼び出し回数・順序そのもの
  • サードパーティ・フレームワークの内部動作: ORMやWebフレームワーク自体の正しさ
  • 言語処理系・標準ライブラリの正しさ: sort()が正しくソートすることを自前でテストする必要はない

これらをテストしても退行保護にはほとんど寄与せず、保守コストだけを増やす。Khorikovはこれを「テストのROI(投資対効果)」という言葉で説明し、価値の低いテストは削除する判断も設計の一部だと述べる。「カバレッジ率を上げるためだけのテスト」はこの原則から見れば負債になりうる(カバレッジの罠についてはテストの質をどう測るかで扱う)。

単体の外へ — 統合テストの範囲問題

「統合テスト」という言葉ほど、人によって指す範囲が異なる用語も少ない。マイクロサービスアーキテクチャの普及とともに、この定義の曖昧さは実務上の問題になった。Martin Fowlerは自身のbliki記事「IntegrationTest」で、範囲による整理を提案している。

  • Broad integration test(広域): 依存する全てのサービス・DB・外部APIを実物で立ち上げて検証する。忠実性は高いが、セットアップが重く不安定になりやすい
  • Narrow integration test(狭域): 外部サービスと通信する部分だけを切り出し、外部側はテストダブルに置き換えて検証する。実行は速く安定するが、実際の挙動とのズレが残る

Fowlerは「broad integration test」は実質的にsystem testやend-to-end testと呼んだ方が誤解が少ないとも述べている(E2Eの系譜はE2Eテストの世代交代を参照)。

Sociable / Solitary — もう1つの切り口

範囲とは別に、Jay Fieldsは著書『Working Effectively with Unit Tests』で、依存先とどう関わるかという軸でsociable(社交的)solitary(孤立的) という対概念を提示した。solitaryは依存先を全てテストダブルに置き換え、sociableは依存先の実物をそのまま使う。「narrow/broad」が主にプロセス境界に注目するのに対し、「sociable/solitary」は同一プロセス内でのオブジェクト間の協調をモックで断ち切るか否かに注目する、やや異なる粒度の軸である。

Testcontainers — 本物の依存をDockerで使う

「モックで済ませると忠実性が犠牲になる」問題への解として広まったのがTestcontainersである。2015年にRichard Northによって開発されたJava向けライブラリを起点とし、Go・.NET・Python・Node.jsなど多言語に移植された。テスト実行時にDockerコンテナで実物の依存(PostgreSQL、Kafka、Redisなど)を使い捨てで起動し、テスト終了後に破棄するという考え方である。インメモリDBが持つ「本物とわずかに挙動が違う」問題を避けられる一方、コンテナ起動のオーバーヘッドで実行速度は落ちる。

契約テストの登場 — Consumer-Driven Contracts

サービスA・B・C・Dと依存が増えるほど、broad integration testの維持コストは指数的に増大する。この問題への解として、Ian Robinsonが2006年にmartinfowler.comへ寄稿した記事「Consumer-Driven Contracts: A Service Evolution Pattern」で提示したのがConsumer-Driven Contracts(CDC、消費者駆動契約) である。

CDCの要点は次の通りである。

  1. APIの利用者(consumer) が、自分が必要とするリクエスト/レスポンスの形を契約として明示的に記述する
  2. その契約は提供者(provider) 側のCIで自動的に検証される。提供者は契約を満たす限り内部実装を自由に変更してよい
  3. 提供者は複数の消費者から契約を集約し、「このAPIを変更したらどの消費者が壊れるか」を実際に結合せずに事前に把握できる

これにより、依存全体を毎回結合して動かさなくても「境界ごとの噛み合わせ」をサービス単位で独立に検証できる。

Pact — CDCを実装した代表的ツール

CDCを実装した代表的なツールがPactである。オーストラリアのREA Group(不動産情報サイト運営)での実務から、コンサルティング会社DiUSが開発したRuby gemとして2013年ごろに生まれ、現在はJVM、JavaScript、.NET、Go、Pythonなど多言語対応の契約テストフレームワークとして使われている。

典型的なワークフローは次の通りである。

  1. 消費者側のテストで、期待するリクエスト/レスポンスのペアを記述し、モックサーバに対してテストを実行する
  2. そのテスト実行から「Pactファイル」と呼ばれる契約定義(JSON)が自動生成される
  3. PactファイルはPact Broker(契約の共有・バージョン管理サーバ)に発行される
  4. 提供者側のCIがPact Brokerから契約を取得し、実際の提供者コードに対してリプレイして検証する(provider verification)

「消費者が書いたテストから契約が自動生成される」という順序が Consumer-Driven の名の由来であり、提供者が一方的に決めた仕様書ではなく、実際の利用実態から契約が導かれる点が特徴である。

Java/Springエコシステムでは、別のアプローチとしてSpring Cloud Contractが使われる。Pactが「消費者が書いたテストから契約を生成する」のに対し、Spring Cloud Contractは「契約(Groovy DSLやYAML)を提供者側が定義し、そこから消費者側のスタブと提供者側の検証テストの両方を自動生成する」という提供者起点の設計を取る。

サービス仮想化という選択肢

契約テストを導入するほどではない、あるいは外部SaaSのように相手側のCIに手が届かない場合、サービス仮想化やスタブサーバで代替するのも一般的である。

ツール 特徴
WireMock 2011年にTom Akehurstが開発したHTTPスタブサーバ
mountebank Brandon Byarsが開発。HTTPに限らずTCP・SMTPなど複数プロトコルに対応
Hoverfly HTTP(S)トラフィックのキャプチャ・再生に強みを持つ

契約テストとの違いは、サービス仮想化が「決め打ちの応答を用意する」だけなのに対し、契約テストは「その決め打ちが提供者側の実際の挙動と一致しているか」まで自動検証する点にある。

まとめ

FIRSTが定める「テストはどう振る舞うべきか」という実行時の性質と、Khorikovの4本柱が定める「テストは何のために存在するか」という価値の性質は、互いに補完的である。そして単体テストの外側では、Fowlerのnarrow/broadという範囲の軸と、契約テストという「結合せずに噛み合わせを保証する」道具が、マイクロサービス化によって指数的に増大した「結合の組み合わせ」問題への解答になっている。最終的に問うべきは「このテストは、コードを変更する自由をどれだけ増やしてくれるか」という一点である。次の記事では、テストの「量」の指標であるカバレッジと、それを補う「質」の指標であるミューテーションテストを扱う。

参考文献

  • Robert C. Martin『Clean Code』(2008年、Prentice Hall)
  • Vladimir Khorikov『Unit Testing Principles, Practices, and Patterns』(2020年、Manning)
  • Roy Osherove『The Art of Unit Testing』(第2版、2013年、Manning)
  • Gerard Meszaros『xUnit Test Patterns: Refactoring Test Code』(2007年、Addison-Wesley)
  • Martin Fowler, “IntegrationTest” / “UnitTest”(martinfowler.com/bliki/)
  • Jay Fields『Working Effectively with Unit Tests』(2014年、Leanpub)
  • Ian Robinson, “Consumer-Driven Contracts: A Service Evolution Pattern”(martinfowler.com、2006年)
  • Pact公式ドキュメント(pact.io)
  • Testcontainers公式ドキュメント(testcontainers.com)
← Back to ソフトウェアテストの系譜