🥜

『単体テストの考え方/使い方』メモ(会話形式でまとめ)

に公開

はじめに

本記事は、**『単体テストの考え方/使い方』(Vladimir Khorikov著、須田智之翻訳、マイナビ出版)**の内容を、会話形式でまとめたものです。

一度自分でメモでまとめたものを生成AIを通して会話形式で文章を再生成してみました。
ちなみにgemini 2.5proです。

先生役と初心者役という役割に設定しており、初心者の方にもわかりやすい構成になっていると思います。


第1章:単体テストの本当の目的って?

工藤: そもそも、単体テストって何のために書くんでしたっけ。「バグを見つけるため」って認識だったんですけど…。

柴崎: いい質問だね!もちろんバグを見つけるのも大事な目的の一つ。でも、もっと根本的な目的があるの。工藤くんさ、「このコード、直したいけど…どこに影響出るか分からなくて触るの怖…」ってなったことない?

工藤: めちゃくちゃあります!まさに先月、古いコードをリファクタリングしようとしたんですけど、怖くて結局やめちゃいました…。

柴崎: あるよね〜!それそれ。ソフトウェアが大きくなると、そういう「変更への恐怖」がどんどん増えていくの。そうなると、コードが資産じゃなくて、触るのが怖い負債になっちゃう。

工藤: 負債、ですか…。確かにそんな感じです。

柴崎: そう。だから単体テストの一番大事な目的は、「安心してコードを変更し続けられる環境を作ること」なの。つまり、テストはリファクタリングの恐怖を取り除くための保険みたいなものなんだよね。

工藤: 保険、なるほど!テストがあれば、思い切ってコードを直せるってことですね。

柴崎: その通り!そのおかげで、アプリが長期的に成長し続けられる(持続可能になる)ってワケ。

【まとめ】
つまり単体テストは、バグ発見のためだけじゃなく、未来の開発者が安心してコードを改善し続けられるようにするための「安全ネット」ってことね。


第2章:「振る舞い」をテストするってどういうこと?

柴崎: さて、ここからが本題。良いテストを書くために、何をテストすればいいと思う?

工藤: ええと…関数の動きが正しいかどうか、ですか?

柴崎: 惜しい!じゃあ「正しい動き」って何かな?ここが超重要なんだけど、単体テストはコードの実装(どう動くか)をテストするんじゃなくて、そのコードの「振る舞い(何が起きるか)」をテストするの。

工藤: 振る舞い…?実装と何が違うんですか?

柴崎: OK!じゃあユーザー登録機能を例に、2つのテストコードを比べてみよっか。まず、実装をテストしちゃってる悪い例ね。

// ❌ 悪い例:実装の詳細をテストしている
it('ユーザー登録時に validateEmail が呼ばれること', () => {
  const spy = vi.spyOn(utils, 'validateEmail') // validateEmailを監視
  registerUser('a@b.com')
  expect(spy).toHaveBeenCalledWith('a@b.com') // 呼ばれたことを確認
})

工藤: あ、これ僕がよく書いちゃうやつです…。「validateEmailっていう関数がちゃんと使われてるかな?」ってのを確認してます。

柴崎: うんうん、そうだよね。でもこれは「どうやって動くか」っていう実装の手順にガッツリ依存しちゃってるの。じゃあ次は、振る舞いをテストしている良い例。

// ✅ 良い例:振る舞いをテストしている
it('無効なメールアドレスを渡すとエラーになること', () => {
  // 「無効なメアドで登録しようとしたら、エラーが起きる」という結果を確認
  expect(() => registerUser('invalid-email')).toThrow('Invalid email')
})

工藤: なるほど!こっちは「validateEmailが呼ばれたか」は気にせずに、「無効なメールアドレスを拒否する」っていうビジネス上のルール(仕様)が守られてるかを確認してるんですね!

柴崎: そゆこと!完璧!実装の詳細は変わる可能性があるけど、「無効なメールアドレスは登録できない」っていう仕様は、めったに変わらないでしょ?

工藤: 確かに!

柴崎: もし実装のテストをしちゃうと、機能は正しく動いてるのに、リファクタリングでコードの内部構造を変えただけでテストが失敗しちゃうの。これを偽陽性(False Positive)って言うんだけど、これが頻発するとエンジニアは「どうせこのテスト、また壊れてるだけでしょ」ってなって、テストを信頼しなくなっちゃうんだ。

工藤: うわ…それはマズいですね。テストが信頼されないなんて…。

柴崎: でしょ?だから、覚えておいてほしいのはこれ。

良いテストは「仕様」に注目し、悪いテストは「実装」に注目する。

工藤: なるほど…。「どう動いたか」じゃなくて「何が起こったか」という結果に注目するんですね!

柴崎: 理解が早い!すばら!

【まとめ】
要するに、テストは「コードの書き方のチェック」じゃなくて「仕様書」であるべきってこと。実装方法を変えても、仕様が変わらなければテストは成功し続けるのが理想なの。


第3章:良いテストが持つ「4つの柱」

工藤: 「振る舞いをテストする」っていうのは分かりました!でも、それだけで良いテストだって言えるんでしょうか?

柴崎: お、良い視点だね。実は、優れた単体テストを支える4つの柱があるの。

  1. 退行に対する保護
  2. リファクタリングへの耐性
  3. 迅速なフィードバック
  4. 保守のしやすさ

工藤: 4つもあるんですね!一個ずつ教えてください。

柴崎: OK!まず「① 退行に対する保護」。これは、コードを変更したせいで「前は動いてた機能が壊れちゃった!」っていう退行バグ(リグレッションバグ)を防ぐこと。テストがあれば、変更を加えた瞬間に「あ、ここ壊したな」って気づける安全ネットになるの。

工藤: なるほど。リグレッション、よく聞きます!

柴崎: 次は「② リファクタリングへの耐性」。これはさっきの話と繋がるんだけど、コードの内部実装を変えても、外から見た振る舞いが同じならテストは失敗しないってこと。これがないと、安心してリファクタリングできないからね。

工藤: さっきのvalidateEmailの例ですね!

柴崎: そうそう!3つ目は「③ 迅速なフィードバック」。コードを一行変えたら、その影響が数秒で分かるのが理想。テスト実行に数分もかかってたら、開発リズムがめっちゃ悪くなるでしょ?だから、単体テストはサクッと終わることが超大事。

工藤: 確かに、待つ時間はストレスですもんね。

柴崎: 最後は「④ 保守のしやすさ」。テストコードもプロダクトコードの一部だから、当然メンテナンスが必要になるの。テスト名を見ただけで「何を検証してるか」が分かる、コードがシンプルで読みやすい、っていうのが大事。良いテストは「動く仕様書」なんだよね。

工藤: テストも読みやすさが大事、と…。この4つが揃って、初めて「良いテスト」なんですね!

柴崎: その通り!この4つのバランスを意識するのがマジで大事なの。

【まとめ】
つまり、良いテストっていうのは、バグから守ってくれて」「リファクタリングを許容し」「すぐに結果が分かり」「誰でもメンテしやすい」っていう4つの特徴を持ってるってことね。


第4章:テストの書き方の基本、「AAAパターン」

工藤: 概念は分かってきたんですけど、いざ書くとなると、どういう構成で書けばいいのか迷っちゃいます。

柴崎: うんうん、分かる。そういうときは、AAA(トリプルエー)パターンっていう型を意識すると、めっちゃ書きやすくなるよ。

工藤: AAAパターン?

柴崎: Arrange(準備)、Act(実行)、Assert(検証) の頭文字をとったもの。テストをこの3つのブロックに分けて書くの。

フェーズ 日本語 やること
Arrange 準備 テストに必要なデータや状態を用意する
Act 実行 テストしたい機能を呼び出す(操作は1つだけ!)
Assert 検証 実行した結果、期待通りの振る舞いになったか確認する

柴崎: 例えば、「非アクティブなユーザーはメールアドレスを変更できない」っていう仕様をテストするコードはこうなるよ。

it('非アクティブなユーザーはメールアドレスを変更できないこと', () => {
  // Arrange (準備)
  const user = new User({ status: 'inactive' });
  const newEmail = 'new@example.com';

  // Act (実行)
  const result = user.changeEmail(newEmail);

  // Assert (検証)
  expect(result.isSuccess).toBe(false);
  expect(result.error).toBe('Cannot change email for inactive user');
});

工藤: おお、Arrange、Act、Assertがコメントで分かれてて、すごく処理の流れが追いやすいです!テスト名とコードを読むだけで、仕様がスッと頭に入ってきますね。

柴崎: でしょ?この型を守るだけで、テストが格段に読みやすくなるし、何がしたいのかが明確になるから、まずはこの構造を真似してみて。

【まとめ】
要するに、テストを書くときは**「準備 → 実行 → 検証」の3ステップ(AAAパターン)**を意識すると、誰が見ても分かりやすい構造になるってこと!


第5章:最重要ポイント!「ドメインロジック」と「副作用」の分離

工藤: ここまでの話、すごく腑に落ちました!でも、実際の開発だと、データベースに書き込んだり、外部のAPIを叩いたりする処理があるじゃないですか。ああいうのって、どうテストすればいいんですか?

柴崎: きた!工藤くん、一番いい質問してくれた!それが、テスト容易性を決める最も重要な設計の話、「副作用」との付き合い方なんだよね。

工藤: 副作用、ですか…?

柴崎: うん。プログラミングでいう副作用っていうのは、関数の外の世界に影響を与えたり、逆に依存したりすること。具体的には、DBへの書き込み、API通信、コンソールへのログ出力とか、全部そう。

工藤: なるほど。そういう処理があると、テストって難しくなりますよね。APIが落ちてたらテストも失敗しちゃうし…。

柴崎: その通り!だから、テストしやすい設計のキモは、ビジネスロジックと副作用を完全に分離することなの。

工藤: 分離…ですか。

柴崎: うん。アプリケーションのコードを2種類に分けるイメージ。

  1. 不変核 (Functional Core)

    • 役割: ビジネスルールに基づいて「決定」だけをするコード。
    • 特徴: 計算や条件分岐だけで、副作用を一切含まない。同じ入力なら必ず同じ結果を返す純粋な部分。
    • テスト: 単体テストでガッツリ品質を保証する。
  2. 可変核 (Imperative Shell)

    • 役割: 不変核の決定に従って、実際に外部とやりとり(副作用)するコード。
    • 特徴: DBに保存したり、APIを叩いたりする「実行」部隊。こっちはできるだけ薄く、ロジックを持たせない。
    • テスト: 統合テストで検証する。

工藤: なるほど!計算みたいな大事なロジックは副作用から隔離して、単体テストでしっかり守る。で、DBアクセスみたいな副作用は、外側の薄い層に追い出す…ってことですか?

柴崎: そう!まさにそれ!その設計こそが、これまで話してきた「リファクタリングへの耐性」とか「迅速なフィードバック」を実現する鍵なのよ。

【まとめ】
つまり、テストしやすいコードを書くコツは、計算や判断をする「純粋なロジック」と、DBアクセスみたいな「副作用」を別々の場所に書くこと。これが最強の設計パターンね。


第6章:【実践】副作用を分離するコードの書き方

工藤: 理論はなんとなく分かりました!実際のコードだと、どうやって分離するんですか?

柴崎: OK!じゃあ「ユーザーのメールアドレスを変更して、通知を送る」っていう処理で、悪い例と良い例を比較してみようか。

❌ 悪い例:ロジックと副作用がごちゃ混ぜ

class UserService {
  // この中で直接APIを叩いちゃってる
  changeEmail(userId, newEmail) {
    // ビジネスロジック
    if (!validateEmail(newEmail)) {
      throw new Error('Invalid email');
    }
    
    // 副作用(DB更新)
    db.users.update({ id: userId, email: newEmail });

    // 副作用(API通信)
    EmailApi.sendChangeNotification(newEmail);
  }
}

工藤: うーん、一見すると普通に見えちゃいますけど…。

柴崎: これだと、UserServiceをテストするために、dbEmailApiっていう2つの外部モジュールをモック(偽物に差し替えること)しないといけないの。テストの準備が超面倒だし、実装を変えたらすぐテストが壊れる、脆いコードなんだ。

✅ 良い例:副作用を外から注入する

柴崎: 次に良い例。副作用を起こす部分を外から渡せるようにするの。これを**依存性の注入(DI: Dependency Injection)**って言うよ。

class UserService {
  // コンストラクタで、副作用を担当するオブジェクトを受け取る
  constructor(userRepository, emailNotifier) {
    this.userRepository = userRepository;
    this.emailNotifier = emailNotifier;
  }

  changeEmail(userId, newEmail) {
    // ビジネスロジック(不変核)
    if (!validateEmail(newEmail)) {
      throw new Error('Invalid email');
    }

    // 副作用は、渡されたオブジェクトに任せる(可変核)
    this.userRepository.update(userId, newEmail);
    this.emailNotifier.notifyChange(newEmail);
  }
}

工藤: あ!dbとかEmailApiを直接使わずに、userRepositoryemailNotifierっていうのを使う形になってるんですね!

柴崎: そう!こうすれば、テストのときは本物のDBやAPIの代わりに、テスト用の偽物(モック)を簡単に渡せるでしょ?

it('メールアドレスを更新して通知すること', () => {
  // Arrange (準備): テスト用の偽物を用意
  const mockRepo = { update: vi.fn() };
  const mockNotifier = { notifyChange: vi.fn() };
  const service = new UserService(mockRepo, mockNotifier);

  // Act (実行)
  service.changeEmail(1, 'new@example.com');

  // Assert (検証): 偽物が正しく呼ばれたかを確認
  expect(mockRepo.update).toHaveBeenCalledWith(1, 'new@example.com');
  expect(mockNotifier.notifyChange).toHaveBeenCalledWith('new@example.com');
});

工藤: すごい!テストコードがめちゃくちゃシンプルになりました!これなら、何をしてるか一目瞭然ですね。

柴崎: でしょ?良い設計は、良いテストコードに繋がる。この「副作用の分離」と「依存性の注入」を覚えるだけで、コードの世界がガラッと変わるよ。

【まとめ】
要するに、外部APIやDBに依存する処理は、直接コード内に書くんじゃなくて、外から渡せる(注入できる)ように設計すると、驚くほどテストが書きやすくなるってこと!


最終章:まとめ

工藤: 柴崎さん、ありがとうございました!今日一日で、テストに対する考え方が180度変わりました。

柴崎: よかった!じゃあ最後に、今日のポイントを5つにまとめておさらいしよっか。

良いテストを書くための5つの指針

  1. 「振る舞い」をテストする:実装の细节ではなく、ユーザーから見た結果に注目!
  2. 「4つの柱」を意識する:退行保護、リファクタリング耐性、迅速さ、保守しやすさのバランスを考える。
  3. 「AAAパターン」で書く:準備・実行・検証の構造で、ストーリーが読めるテストにする。
  4. 「副作用」を分離する:ビジネスロジック(不変核)と外部通信(可変核)は混ぜない!
  5. 「依存性の注入(DI)」を活用する:副作用を外から渡せるようにして、テストしやすさを爆上げする。

工藤: この5つを意識すれば、僕も「良いテスト」が書けるようになりますかね…?

柴崎: もちろんだよ!最初から完璧じゃなくていいの。まずは新しく書くコードから、この原則を一つでもいいから試してみて。困ったら、いつでも声かけてね。

工藤: はい!ありがとうございます!明日から早速やってみます!

柴崎: うん、期待してる!良いテストを書いて、未来の自分たちを助けてあげよっ!


想定Q&Aセッション

Q1. 4つの柱は全部満たす必要がある?

工藤: 柴崎さん、前に教わった「良いテストの4つの柱」なんですけど、あれって全部100%満たす必要ってあるんですか?たまに「迅速なフィードバック」を優先すると、「退行に対する保護」が少し弱くなる…みたいなトレードオフがある気がして。

柴崎: お、めっちゃ良いところに気づいたね!工藤くんの言う通り、この4つは常にトレードオフの関係にあるの。全部100点満点っていうのは現実的じゃないんだよね。

工藤: やっぱりそうなんですね!

柴崎: そう。例えば、退行保護を最強にしようとして、隅々までテストを書くと、実行時間が長くなって迅速なフィードバックが失われたり、テストの保守コストが爆上がりしたりする。だから、「このテストの目的は何か?」を考えて、どの柱を優先するか判断するのが大事なの。重要なビジネスロジックなら退行保護を、UIの細かい部分なら迅速さを優先する、みたいな感じでね。

工藤: なるほど…常に「完璧」を目指すんじゃなくて、状況に応じてバランスを取るのが大事なんですね!

柴崎: そゆこと!完璧主義にならず、プロジェクトにとって一番価値のあるバランスを見つけるのが、腕の見せ所ってワケ。

Q2. Privateメソッドもテストすべき?

工藤: publicなメソッドをテストするのは分かったんですけど、クラス内部でしか使わないprivateなメソッドって、テストした方がいいんでしょうか?

柴崎: あー、これ結構みんな悩むやつだよね。結論から言うと、原則としてprivateメソッドを直接テストする必要はないよ。

工藤: え、そうなんですか?でも、ロジックが詰まってたら不安じゃないですか?

柴崎: 気持ちは分かる!でも思い出してみて。私たちがテストしたいのは「実装」じゃなくて「振る舞い」だったでしょ?privateメソッドは、あくまでpublicメソッドっていう「振る舞い」を実現するための実装の詳細なの。

工藤: あ、そっか!

柴崎: そう。だから、publicメソッドをテストして、その振る舞いが正しければ、内部で使われてるprivateメソッドも間接的にテストできてるってことになる。もしprivateメソッドを直接テストしちゃうと、将来リファクタリングでそのprivateメソッドを消したり名前を変えたりしただけで、テストが壊れちゃう。それって、まさに「リファクタリングに弱いテスト」だよね。

工藤: 確かに…。publicな振る舞いが変わってないのに、テストが失敗するのは避けたいですもんね。

柴崎: そうなの。もし「このprivateメソッド、どうしても単体でテストしたい…」って思ったら、それは**「クラスの責務が大きすぎる」っていう設計のアラーム**かも。そのロジックを別のクラスに切り出して、publicなメソッドとしてテストできないか、考えてみるといいよ。

Q3. モックはどこまで使うべき?

工藤: DI(依存性の注入)でテストが書きやすくなるのは分かったんですけど、逆に何でもかんでもモックにしちゃって、「結局、何をテストしてるんだっけ?」ってなっちゃうことがあるんです…。

柴崎: あー、あるある!モック地獄ってやつね。モックは便利だけど、使いすぎはNG。目安としては、「自分たちがコントロールできない外部システム」だけをモックするのが基本かな。

工藤: 外部システム、ですか。

柴崎: うん。例えば、外部のAPI、DB、ファイルシステム、あとは現在時刻とかね。こういうのは、テスト実行のたびに結果が変わったり、そもそもテスト環境で動かなかったりするでしょ?だからモックする。逆に、自分たちで実装した他のクラスは、なるべく本物を使うのが理想。その方が、クラス同士が連携したときのリアルな振る舞いを検証できるからね。

Q3.5.(補足)自分たちのクラスをモックしないテストの例

工藤: 柴崎さん、さっきの「自分たちが実装した他のクラスは、なるべく本物を使うのが理想」っていう話、もう少し具体的なコードで見てもいいですか?いまいち、モックする時としない時の違いがイメージできなくて…。

柴崎: もちろん!じゃあ、分かりやすい例でいってみようか。例えば、ECサイトで「商品の割引価格を計算する」っていう機能を実装するとするね。

柴崎: まず、特定の割引ルールを計算する、シンプルなDiscountCalculatorクラスを自分たちで作ったとしよう。

// 自分たちで実装したクラス①:割引ルールを計算する
export class DiscountCalculator {
  // 10%オフにするメソッド
  calculateTenPercentOff(price: number): number {
    return price * 0.9;
  }
}

柴崎: そして、このDiscountCalculatorを使って、最終的な価格を計算するPriceCalculatorクラスも作ったとする。

// 自分たちで実装したクラス②:↑のクラスを使って最終価格を出す
import { DiscountCalculator } from './DiscountCalculator';

export class PriceCalculator {
  private discountCalculator: DiscountCalculator;

  constructor() {
    // 内部で自分たちのDiscountCalculatorをnewしている
    this.discountCalculator = new DiscountCalculator();
  }

  // 割引を適用した最終価格を返すメソッド
  calculateFinalPrice(price: number): number {
    // 内部クラスのメソッドを呼び出す
    return this.discountCalculator.calculateTenPercentOff(price);
  }
}

工藤: なるほど。PriceCalculatorが、内部でDiscountCalculatorに依存してるわけですね。

柴崎: そういうこと。じゃあここで、PriceCalculatorをテストする時に、DiscountCalculatorモックしてしまう悪い例から見てみよう。

❌ 悪い例:自分たちのクラスをモックしてしまうテスト

import { PriceCalculator } from './PriceCalculator';
import { DiscountCalculator } from './DiscountCalculator';

// vi.mockを使って、DiscountCalculator全体をモック対象にする
vi.mock('./DiscountCalculator');

it('内部のDiscountCalculatorを呼び出していること', () => {
  // 準備:モックされたDiscountCalculatorのインスタンスを取得
  const mockedDiscountCalculator = new DiscountCalculator();
  // 戻り値を「900」に固定する
  vi.spyOn(mockedDiscountCalculator, 'calculateTenPercentOff').mockReturnValue(900);

  // 実行
  const priceCalculator = new PriceCalculator();
  const finalPrice = priceCalculator.calculateFinalPrice(1000);

  // 検証
  expect(finalPrice).toBe(900);
  // 【問題点】「calculateTenPercentOffが呼ばれたか」という実装の詳細をテストしてしまっている
  expect(mockedDiscountCalculator.calculateTenPercentOff).toHaveBeenCalledWith(1000);
});

工藤: うーん…一見、ちゃんとテストできているように見えますけど…。

柴崎: このテストの問題点は、DiscountCalculatorの10%オフの計算が正しいか」を全く検証できていないことなの。ただ「calculateTenPercentOffっていうメソッドが呼ばれたか」っていう、PriceCalculator実装の细节をチェックしてるだけ。もし将来、DiscountCalculatorのメソッド名をcalculateCampaignDiscountに変えたら、PriceCalculatorの振る舞いは何も変わってないのに、このテストは壊れちゃう。

工藤: ああっ!本当ですね!これだと、リファクタリングに弱いテストになっちゃうんだ…。

柴崎: その通り!じゃあ次に、モックを使わずに本物のオブジェクトを使う良い例を見てみよう。

✅ 良い例:本物のクラスをそのまま使ってテストする

import { PriceCalculator } from './PriceCalculator';

it('1000円の商品に10%割引を適用すると900円になること', () => {
  // 準備:テスト対象を普通にインスタンス化するだけ
  const priceCalculator = new PriceCalculator();

  // 実行
  const finalPrice = priceCalculator.calculateFinalPrice(1000);

  // 検証:最終的な振る舞いの結果だけを確認する!
  expect(finalPrice).toBe(900);
});

工藤: めちゃくちゃシンプル!モックの準備もいらないし、コードがすごくスッキリしてます。

柴崎: でしょ?このテストは、PriceCalculatorが内部で何を使ってるかなんて気にしてない。ただ**「1000円を渡したら、最終的に900円が返ってくる」という振る舞い**だけを検証してる。これなら、DiscountCalculatorの内部実装が変わろうが、PriceCalculatorDiscountCalculatorを使うのをやめて自前で計算するようになろうが、最終的な振る舞いが変わらない限り、このテストは成功し続けるの。

工藤: なるほど!こっちの方が、断然「振る舞い」をテストしてますね!自分たちでコントロールできるクラスは、モックせずに本物同士を連携させて、その最終結果をテストする方が、より価値が高くて頑丈なテストになるんですね。よく分かりました!

Q4. TDD(テスト駆動開発)ってやった方がいい?

工藤: 最近、TDD(テスト駆動開発)っていう言葉をよく聞くんです。先にテストを書いてから実装するっていう…。あれって、やった方がいいんでしょうか?

柴崎: TDDね!あれは超強力な開発手法だよ。ただ、全員が絶対にやるべき!っていう銀の弾丸じゃないかな。

工藤: そうなんですか?メリットが大きそうなのに。

柴崎: メリットは本当に大きいよ。自然とテストしやすい設計(DIとか副作用の分離)になるし、必要なコードだけを書くようになるから無駄がない。でも、慣れないうちは、最初にどんなテストを書けばいいか分からなくて、逆に時間がかかっちゃうこともあるの。

工藤: 確かに、最初にテストを書くのって難しそうです…。

柴崎: うん。だから、チームの開発スタイルやメンバーの習熟度によるかな。いきなり全部TDDでやろうとせず、まずは**「実装が終わった直後に、忘れないうちにテストを書く」**ことから始めてみるのがおすすめ。テストを書く文化が根付いてきたら、次のステップとしてTDDに挑戦してみると、スムーズに移行できると思うよ。

Q5. 1つのテストケースにexpectは1つだけ?

工藤: たまに「1つのテスト(itブロック)には、expect(アサーション)は1つだけにすべき」っていうルールを聞くことがあるんですけど、本当ですか?

柴崎: ああ、それもよく議論になるテーマだね。そのルールの本質は、**「1つのテストケースでは、1つの振る舞いだけを検証すべき」**ってことなの。

工藤: 1つの振る舞い、ですか。

柴崎: そう。例えば、「ユーザーが無効なメールアドレスで登録しようとしたら、エラーオブジェクトが返る」という振る舞いをテストしたいとするじゃん?この場合、エラーオブジェクトの中に「成功フラグがfalseであること」と「エラーメッセージが特定のものであること」っていう複数の状態が含まれてるよね。

it('無効なメアドならエラーを返す', () => {
  const result = registerUser('invalid-email');

  // これらは全て「エラーを返す」という1つの振る舞いを構成する要素
  expect(result.isSuccess).toBe(false);
  expect(result.error.code).toBe('INVALID_EMAIL');
  expect(result.error.message).toBe('Email is not valid');
});

柴崎: このexpectは3つあるけど、検証してるのは「エラーが返る」っていう1つの概念的な振る舞いでしょ?こういう場合は、expectが複数あっても全然OK。

工藤: なるほど!

柴崎: 逆にダメなのは、1つのテストケースで「正常系の振る舞い」と「異常系の振る舞い」みたいに、全然違うシナリオを同時に検証しちゃうこと。expectの数にこだわりすぎるんじゃなくて、**「このテストで検証したいことは、一言で説明できるか?」**を考えてみると、自然と良いテストになるよ。

参考文献

『単体テストの考え方/使い方』Vladimir Khorikov (著), 須田智之 (翻訳), マイナビ出版

Discussion