🔯

単体テストと学派のはなし

に公開

こんにちは!
株式会社ココナラ QA に所属しているゴローです。

こちらは 株式会社ココナラ Advent Calendar 2025 16日目の記事です。

はじめに

生成AI、非常に便利で最高ですよね!
しかし生成AIに単体テストを書いてもらった時に、テストをパスさせるためだけの不適切なコードを出力された経験はないでしょうか? 私は何度もあります...🥲

ココナラのバックエンドチームでは単体テストを拡充するにあたり、これまでのPRレビューで蓄積されてきた暗黙知を明文化し、テストポリシーを作成しました。
このポリシーの中で古典学派(Classicist)のアプローチを採用しています。

本記事では、単体テストの2大学派である「古典学派」と「ロンドン学派」について記載します。そして、生成AI時代においてなぜテストポリシーの明文化が重要なのかについても触れます。

単体テストの2大学派

単体テストの書き方には、長年議論が続いているテーマがあります。

Martin Fowler氏は、単体テストに対する考え方の違いを「学派」と呼びました。開発者によって信じる正義が異なるため、これらはしばしば宗教論争に発展します。大きく分けて以下の2つの派閥が存在します。

🇬🇧 ロンドン学派 (London)

別名: モック主義者 (Mockist)

  • モックを好む(依存先はすべて偽物に置き換える)
  • 検証するのは「相互作用(呼び出し)」
  • 「単体」とは「コードの単位(クラス)」である

🏛️ 古典学派 (Classicist)

別名: デトロイト学派

  • 実物を好む(可能な限り本物のオブジェクトを使う)
  • 検証するのは「状態」や「結果」
  • 「単体」とは「振る舞いの単位」である

テストの書き方はどう変わるのか?

テスト対象関数が処理関数Aと処理関数Bを使用するケースを考えてみます。

1. ロンドン学派のアプローチ

ロンドン学派は徹底的な隔離を行います。テスト対象以外はすべてモック(偽物)にします。

ロンドン学派

// ロンドン学派の例: すべての依存をモック化
test('ユーザー作成処理のテスト', () => {
  // DB保存とメール送信をモック化
  const mockSaveToDb = jest.fn();
  const mockSendEmail = jest.fn();

  // テスト実行
  createUser('太郎', mockSaveToDb, mockSendEmail);

  // 呼び出しを検証(相互作用のテスト)
  expect(mockSaveToDb).toHaveBeenCalledWith({ name: '太郎' });
  expect(mockSendEmail).toHaveBeenCalledWith('太郎');
  expect(mockSaveToDb).toHaveBeenCalledTimes(1);
  expect(mockSendEmail).toHaveBeenCalledTimes(1);
});

このテストで確認することは

  • それぞれの関数が何回呼び出されたか
  • それぞれの関数に正しい引数が渡されたか

というような関数の呼び出し(相互作用)をテストします。

2. 古典学派のアプローチ

古典学派は可能な限り現実的なシミュレーションを行います。DBや外部APIなどの「扱いにくい依存」以外は、本物のオブジェクトを使います。

古典学派

// 古典学派の例: 外部通信のみモック化
test('ユーザー作成処理のテスト', async () => {
  // メール送信のみモック化(外部通信のため)
  const mockSendEmail = jest.fn();

  // テスト用DBをセットアップ
  const testDb = setupTestDatabase();

  // テスト実行(本物のDB保存関数を使用)
  await createUser('太郎', testDb, mockSendEmail);

  // 最終的な状態を検証
  const savedUser = await testDb.findUser('太郎');
  expect(savedUser).toEqual({ name: '太郎' });
  expect(savedUser.createdAt).toBeDefined();

  // 外部通信は呼び出しを確認
  expect(mockSendEmail).toHaveBeenCalledWith('太郎');
});

このテストで確認することは

  • DB保存処理の実施後、DBが正しく更新されているか
  • テスト対象関数の戻り値は期待通りか

というような最終的な結果(状態)をテストします。

それぞれの学派のメリット・デメリット

観点 ロンドン学派 古典学派
テスト実行速度 ✅ 高速
依存先が偽物のため、DBアクセスやネットワーク通信がなく高速
⚠️ 遅くなる可能性
実物のオブジェクトやDBを使うため時間がかかる
問題箇所の特定 ⚠️ 隔離性は高い
テスト対象クラスのみ検証するが、実装の問題かモックの問題かの切り分けが必要
⚠️ テストの粒度次第
適切な粒度なら特定可能だが、複数コンポーネントが絡む場合は工夫が必要
メンテナンスコスト ❌ 高い
依存先の変更時に、すべてのモックも変更が必要
✅ 低い
モック箇所が少ないため、修正箇所も少ない
リファクタリング耐性 ❌ 弱い
実装の詳細に強く結びつき、テストコードも大幅変更が必要
✅ 強い
振る舞いをテストするため、内部構造の変更に影響されにくい
実際の動作との近さ ❌ 離れている
モックと実物の挙動が異なる場合、統合時に問題が発覚
✅ 近い
本物のオブジェクトを使い、現実的なシミュレーションが可能
依存先未実装時 ✅ テスト可能
モックを使えば、依存先が完成していなくてもテストを先に書ける
❌ テストを書きにくい
本物のオブジェクトを使うため、依存先の完成が必要
テストデータ準備 ⚠️ モックの設定
DB不要だが依存関係が多いと設定コードが複雑化
⚠️ データの準備
DBや外部リソースのセットアップと後片付けが必要
統合時の問題発見 ❌ 遅い
モックと実物の差異により統合時に初めて問題が発覚する可能性
✅ 早い
実物を使うことでコンポーネント間の連携問題を早期発見

個人的感想

テストではモックも使いますが、ベースとしては古典学派の考えが好きです🏛️

理由としてはモック箇所をいちいち修正するのが手間になること、デグレが発生する可能性が高いと考えられるためです。

例えば影響範囲の広い処理の修正が発生した場合、修正したファイルのテストは直したものの、別ファイルで設定されているモックを更新し忘れるケースがあります。この場合、テストは通りますが実際にはデグレが発生しています。
そういった誤りを防ぐためにもありのままをテストする古典学派をベースに置くべきだと思います。

テスト呼び出し時のみ環境変数でDBのURLをテスト用にすり替えたり、DB接続関数をテスト用関数にoverrideする等やり方は色々あると思います。テスト用データの準備が大変ではありますが、将来の保守しやすさを考えると惜しまずに時間をかけるべきではないでしょうか。

生成AI時代においてなぜテストポリシーの明文化が重要なのか

1. AIへの指示の精度を上げるため

単に「テストを書いて」と指示すると、プロジェクトのコードを参考にしてくれますが「実装のしやすさ」を優先して、過剰なモックを使用したテストを生成しがちです(ロンドン学派的なアプローチの方がセットアップのコード量が少なくて済む場合が多いため)。

しかし、ポリシーとして「古典学派を採用する」「原則としてモックは外部通信のみ」と明文化されていれば、それをプロンプトに含めることができます。
このように明文化されたルールを渡すことで、AIはチームの思想に沿った「使えるコード」を出力してくれるようになります。

2. レビュー基準を統一し「なんとなくOK」を防ぐため

人間が書いたコードであれば「なぜここでモックしたの?」と意図を聞くことができますが、もしポリシーが明文化されていないと、レビュワーも

「テスト通ってるし、AIが書いたからまあいいか」
「モックが多いけど、書き直すのも面倒だし...」

という心理になり、保守性の低いテストコードがプロダクトに混入してしまいます。

「古典学派(Classicist)で書く」 という明確な合意があれば、AIが出力した過剰なモックコードに対して、自信を持って修正が必要と判断できます。生成AIが広く使われる現在だからこそ、人間が「良し悪しを判断するための物差し」を明確に持っておくべきです。

おわりに

今回は単体テストにおける学派について記載しました。
生成AIの力を最大限に借りつつ、保守しやすいテストコードを積み上げていくために、皆さんのチームでもテストポリシーについて一度話し合ってみてはいかがでしょうか?

参考


明日は Tomonori Nakada さん による「リリースから振り返る2025年のココナラアプリ開発」です。

ココナラでは積極的にエンジニアを採用しています。
採用情報はこちら。
https://coconala.co.jp/recruit/engineer/

カジュアル面談希望の方はこちら。
https://open.talentio.com/r/1/c/coconala/pages/70417

Discussion