飛ばない豚
記事一覧へ戻る
この記事を読むのにかかる時間:約7分

単体テストのアンチパターン

カテゴリー#アンチパターン / 単体テスト

マイナビ出版の『単体テストの考え方/使い方』を参考に、単体テストにおけるアンチパターンをいくつか列挙してみる。(後半の統合テストの部分は読んでいない1)

本書の英語版は2020年1月、日本語版は2022年12月に発売されている。英語版/日本語版 ともに Amazonレビューは4.5を超えており、ここ数年の「ソフトウェアテスト本」を代表する1冊といったところか。

『単体テストの考え方/使い方』の書影
単体テストの考え方/使い方

Amazonで見る ↗

前提条件・用語

AAAパターン

テストケースはAAAパターンで記述する。AAAパターンとは、テストメソッドを Arrange (テスト実行のために必要なものを準備)、Act(ターゲットコードの実行)、 Assert(ターゲットコードの挙動が想定通りか確認) の3フェーズに分けて記述することである。2

古典学派

本書は一貫して古典学派(デトロイト学派)の単体テスト設計を是としている。古典学派とは、以下のような特徴を満たすテスト設計のこと。3

  • 1単位のコード(クラス)ではなく、1単位の観察可能な振る舞いを検証する
  • テストケース同士を隔離し、どんな順番で何度実行してもテスト結果が変わらないようにする

SUT

System Under Test の頭字語。テスト対象のシステム(メソッドと読み替えてもいいと思う)。

テスト・フィクスチャ

テストを実行するために、決まった状態に準備しておくオブジェクトやデータのこと。例えば、SUTに渡す依存オブジェクト、データベース内のデータ、ファイルなどが該当する。テストの実行前に決まった状態にしておくことで、同じ条件で振る舞いを検証できる。


以下は、本書の方針に沿って整理したアンチパターンである。

テストメソッド内に同じフェーズを複数用意する

Arrange -> Act -> Assert -> Act -> Assert のように、同じフェーズが同じテストメソッドに複数回登場してはいけない。

1つのテストケースの中で複数の振る舞いを検証していることになる。単体テストは「1単位の振る舞い」を検証すべきである。

if文を使用する

1つのテストケースの中で複数のことを検証しているため。条件分岐によって実行する手順や検証内容が変わると、テストが読みにくくなる。

Actフェーズのコードが2行以上になっている

SUTのメソッドの設計がまずいことを示唆しているため。何がまずいかというと、1つの操作を完了するために複数のメソッド呼び出しが必要になっていること。本来、1つの操作は1つのメソッドで完結するべき。呼び出し元が2つ以上のメソッドを順番に呼ばないと状態の整合性を保てないなら、カプセル化に失敗している。

確認フェーズ後に後始末フェーズを設けている

後始末とは、テスト時に作成したファイルを削除したりデータベースの接続を切ったりすること。

単体テストのテストケース間で、あるテストの変更が別のテストの結果に影響するような依存を共有してはいけない。テストケース同士は隔離されるべきである。ファイルやデータベースを実際に使い、後始末フェーズが必要になるのは統合テストである。

テスト・フィクスチャをテストクラスのコンストラクタで準備する

コンストラクタはテストケース間の結びつきを強める。コンストラクタのコードを変えると、そのテストクラス全体に影響が出る。コンストラクタでは、テストケースの修正の独立性を守れない。

また、テストの内容を把握するために、テストメソッドとコンストラクタの両方を見る必要がある。これは可読性の低下に繋がる。

対処法: ファクトリメソッドを使う

各テストケースの準備フェーズでファクトリメソッドの引数に値を渡すことで、テストケース間の結びつきを弱めることができる。ファクトリメソッド名と渡される引数を見ることで、テストケースの準備フェーズの内容を理解できるようになる。例外として、すべてのテストケースで同じテストフィクスチャを使っているのであれば、コンストラクタを使っても良い。

テストメソッド名に厳格な命名規則をつける

複雑な振る舞いを表現することに限界が出てくるため。開発者じゃない人にも、どのような検証をするのか伝わるような名前をつける必要がある。

テスト対象のメソッド名をテストメソッド名に含める。

単体テストはコードではなく振る舞いをテストするためである。メソッド名を含めてしまうと、コードとテストが結びついていることになる。

テストメソッド名に should be などの要望が入っている

単体テストは、1単位の振る舞いについての事実を伝えるものである。should や must のような主観的なことは書かない。 XX_is_invalid のような書き方でよい。

SUTの実装の詳細を検証しているテスト

振る舞いが変わっていないリファクタリングでもテストが失敗し、アプリケーションコードの改善を妨げるため。アプリケーションの振る舞いは正常なのにテストが失敗することを、偽陽性(false positive)という。 実装の詳細を検証するテストは、観察可能な振る舞い(最終的な結果)を検証していないため、偽陽性を生みやすい。例えば、1 + 2 = 3 において観察可能な結果は 3 であり、1 + 2 と 2 + 1 のどちらの順番で加算したかではない。実装の詳細を検証するテストは、この加算の順番まで検証している。

ホワイトボックス・テストで実装の詳細を検証する

ホワイトボックステストとは、システムの内部構造を分析してテストを設計する手法である。内部構造を参考にすること自体と、実装の詳細を検証することは区別する必要がある。実装の詳細まで検証すると、テストケースとSUTの内部のコードが密接に結びつき、偽陽性が生じやすくなる。

スタブへの呼び出しを検証する

スタブとは、テストダブルの一種であり、SUTへの入力を模倣するために使われる。例えば、データベースからデータを取得するときなどである。一方、SUTから依存先への出力方向のやり取りを模倣し、その呼び出しを検証するテストダブルはモックと呼ばれる。

SUTがスタブを呼び出すのは、SUTが最終的に出力する結果を生むための過程に過ぎないから。スタブを何回呼んだか、どの引数で呼んだかといったやり取りは、アプリケーションの観察可能な振る舞いにはならない。検証するのは、スタブから受け取った入力をもとにSUTが生み出す最終的な結果である。

まとめ

本書が口酸っぱく説いていることを2点にまとめる。

  • 実装の詳細ではなく、観察可能な振る舞いをテストする
  • テストケース同士を隔離し、テストケース同士が影響を与え合わないようにする

また本記事で紹介したアンチパターンは、あくまで『単体テストの考え方/使い方』のつまみ食いに過ぎない。他にも参考になる記述が多くあった。 例えば序盤の古典学派とロンドン学派の比較からは、「そもそも単体とはなにか・何をもって単体とみなすのか」といった定義が展開される。中盤の「良い単体テストを構成する4本の柱」では、単体テストを設計する際の指針が示されている。


脚注

Footnotes

  1. そもそも俺がマイナビ出版の技術書とオライリー本を最後まで通読するのは稀である。途中で力尽きる(飽きる)。 ↩

  2. The Arrange, Act, and Assert (AAA) Pattern in Unit Test Automation ↩

  3. Detroit and London Schools of Test-Driven Development ↩