単体テストの考え方 / 使い方【第1部】を読んで
はじめに
ソフトウェア開発において、品質を担保するために欠かせないのが「テスト」です。中でも「単体テスト」は、開発の初期段階でバグを発見し、システムの品質を向上させる上で非常に重要な役割を担います。しかし、「単体テスト」と一口に言っても、その定義や手法は多岐にわたります。
ここでは、『単体テストの考え方 / 使い方』(マイナビ出版)の内容を読んで、私が解釈した単体テストの基本的な概念から、その価値を最大限に引き出すための考え方までを解説します。
実際に本を読んでみると理解度が変わるので、気になれば読んでみてください。違う理解等があれば、コメント等お願いします😌
第1部:単体テストとは?
単体テストとは、その名の通り、システムの「1単位(ユニット)」を検証するテストです。具体的には、以下の3つの条件を満たすものを指します。
- 単位の振る舞い(a unit of behavior)を検証すること:特定の機能やメソッドなど、最小限の単位が意図した通りに動作するかを確認します。
- 実行時間が短いこと:テストは頻繁に実行されるため、素早くフィードバックが得られるよう、短時間で完了する必要があります。
- 他のテスト・ケースから隔離された状態で実行されること:テストが他のテストの影響を受けたり、他のテストに影響を与えたりしないよう、独立して実行されることが重要です。
単体テストにおける2つの学派
単体テストには、主に以下の2つの学派が存在します。
-
古典学派(デトロイト学派)
テスト対象のシステムが依存する「協力者オブジェクト」を、テストダブル(テストのために代替するオブジェクト)として扱わず、実際のオブジェクトを使用します。 -
ロンドン学派(モック主義者)
テストダブル(特にモック)を使って協力者オブジェクトからテスト対象を隔離し、テスト対象単体の振る舞いを検証します。
両者の共通点は、どちらも一つのシステムを対象としている点です。
異なる点は、そのシステムが動作するために必要な「協力者オブジェクト」をテストダブルとして扱うかどうかにあります。
依存の種類とテストダブルの利用
システムが依存するオブジェクトには、大きく分けて2種類あります。
-
共有依存
複数のテスト間で共有されるオブジェクト(例:データベース、ファイルシステム、ネットワークサービスなど)。これらはテストの独立性を損なったり、実行時間を長くしたりする原因となるため、テストダブルの使用が推奨されます。 -
プライベート依存
特定のテスト対象にのみ関連する他のクラスオブジェクト。- 可変依存:状態が変化する可能性のあるプライベート依存。
- 不変依存:状態が変化しないプライベート依存。
古典学派は、共有依存のみスタブを作成します。
ロンドン学派は、共有依存だけでなく、プライベート依存の中でも可変依存に対してのみスタブを使用します。不変依存は参照されても変更されないため、テストダブルにする必要がないと考えます。
統合テスト、E2Eテストとの違い
単体テスト以外にも、システムの品質を測るためのテストとして「統合テスト」と「E2Eテスト」があります。
-
統合テスト
テスト対象以外のいくつかの外部システムに依存するテストです。複数のコンポーネントが連携して正しく動作するかを検証します。 -
E2Eテスト(End-to-End Test)
テスト対象以外のほぼ全ての外部システムに依存し、ユーザーが実際にシステムを使用するのと同様の流れで、システム全体が正しく機能するかを検証します。
古典学派の単体テストは、ロンドン学派の観点から見ると、依存する実際のオブジェクトを含んでテストするため、「統合テスト」に近いとみなされることがあります。
E2Eテストは、その性質上、準備や実行に時間がかかり、運用コストも高いため、単体テストや統合テストが十分に機能していることを確認してから実施するのが一般的です。
テストの3つのフェーズとAAAパターン
テストコードは、通常、以下の3つのフェーズに分けて記述されます。これをAAA(Arrange-Act-Assert)パターンと呼びます。
-
準備(Arrange / Given)
テストを実行するために必要なデータや環境を準備するフェーズです。 -
実行(Act / When)
テスト対象のシステムやメソッドを実行するフェーズです。 -
確認(Assert / Then)
実行結果が期待通りであるかを確認するフェーズです。
TDD(テスト駆動開発)では、まずAssertから書き始めることが多いです。一方、プロダクトコードを先行して開発する場合は、Arrangeから書き始めるのが自然です。
各フェーズのコード量としては、通常
単体テストで避けるべきこと
良い単体テストを書くためには、以下の点を避けるべきです。
-
同じフェーズ(AAA)を繰り返すこと
例えば、一つのテストケース内でArrange, Act, Assertのセットを複数回繰り返すのは避け、それぞれ別のテストケースに分離しましょう。 -
if文を使用すること
テストケース内に条件分岐を設けるのではなく、条件ごとに独立したテストケースを作成しましょう。
これらのプラクティスは、テストコードの可読性と保守性を高めます。
単体テストの名前の付け方
テストメソッドには、テスト対象のプロダクトコードのメソッド名をそのまま付けるべきではありません。
単体テストで検証したいのは、「振る舞い」であり、特定の「コード」ではないからです。
メソッド名を付けたテストは、プロダクトコードと強く結びつき、リファクタリング時にテストが壊れやすくなるなど、テストスイートの保守に悪影響を及ぼします。
テストメソッド名は、「何を」「どのような状況で」「どうなることを期待するのか」など、検証したい振る舞いを明確に記述するようにしましょう。
パラメータ化テスト
一つのテスト対象システムに対して、複数のテストケース(正常系、異常系など)がある場合、それらを簡潔に記述するのにパラメータ化テストが有効です。共通のロジックを持つテストケースをまとめて記述できます。
ただし、全てのケースをパラメータ化すると、かえってテストが読みづらくなることがあります。
テストが読みづらいということは、「何を・どんな振る舞いを検証しようとしているのか」が分かりづらくなることを意味します。
そのため、正常系は個別に記述し、異常系など共通化しやすい部分のみをパラメータ化テストにするといった使い分けが推奨されます。
確認フェーズ(Assert)を読みやすくする
確認フェーズの記述は、より人間が理解しやすい言葉で書くことで、何を期待しているのかが分かりやすくなります。
例えば、expect(value).to be_true
のように直接的な比較を行うよりも、RSpecのmatcherにある be_xx(例:be_revoked
) のようなカスタムマッチャーを使うことで、意図を明確に表現できます。
感想
「単体テスト」「統合テスト」「E2Eテスト」は境界が曖昧で、ある観点ではどちらにでも属することができる。大事なのはそのテストを行うことによって得られる価値が何か?であり、定義にとらわれすぎずプロダクトコードのバグをいかに容易に発見できて、修正しやすいかを求めること。と感じた。
Discussion