どんな本か
単体テストの価値を最大限引き出す「良い」テストを書くための原則と実践方法のパターンを紹介する本。
構成
大きく4つのパートに分かれている。
- 単体テストの概要・一般的に広く知られている単体テストの原則の紹介 (第1〜3章)
- 良い単体テストを構成するものとは何か&どうすれば既存のテストをより価値のあるものにすることができるのかの解説 (第4〜7章)
- 結合テストについて (第8〜10章)
- 単体テストでよくあるアンチパターンの紹介 (第11章)
今回は、第1部 「単体 (unit) テストとは」 のメモ
1. なぜ、単体 (unit) テストを行うのか?
単体テストを行う目標と、良いテストと悪いテストをどのように区別するのか?ということのについての概要
1-1.単体テストの現状
- 「単体テストは、必ずやるべきことである」という価値観が当然になった
- ソフトウェア開発で、当たり前に単体テストは導入されるようになった
- しかし、単体テストを導入したプロジェクトでもその効果を得られていないことがままある
- 実装済みの機能に新しいバグがいくつも見つかる、など
- その理由は、適切な単体テストを作成できていないから
- 良い単体テストとは何か?
1-2. なぜ単体テストをするのか
プロダクトの成長を「持続可能」にするため
単体テストを実施する場合と実施しない場合で、プロダクトの成長にどんな違いがあるのか
- 単体テストがある場合
- 初期 : プロダクション・コードの設計・作成に加えて、テスト・ケースの作成・テスト・コードの作成をするため、時間がかかる
- 中期~ : プロダクトの規模が大きくなっても、一定の速度で成長する
- 単体テストがない場合
- 初期 : プロダクション・コードの設計・作成のみを行うため、テストを実施する場合に比べて速く開発が進む
- 中期~ : プロダクトの規模が大きくなるにつれて、開発速度が落ち、成長が停滞する
なぜ単体テストがないと、開発速度が落ちるのか?
- バグが混入する可能性が高まるため
- バグの発見が遅れるため
- コードの変更が難しくなるため
なぜ単体テストがあると、開発速度を維持できるのか?
- バグが混入する可能性が低くなるため
- バグを早期に発見できるため
- コードの変更が容易になるため
- テストがあることで、既存の機能が正しく振る舞うことを保証することができている
単体テストの欠点
- 用意するのに労力がかかる
- 保守するのに労力がかかる
テストコードもプロダクションコードと同じように技術的負債である。
1-3. 良い単体テストと悪い単体テストでは、プロダクトの成長にどんな違いがあるのか
最終的に、テストを実施しない場合と同じ結果になる
- 悪い単体テストの場合
- 初期 : 最初のうちは。一定の開発速度を保っている
- 中期~ : テストがない場合と同じく、プロダクトの規模が大きくなるにつれて、開発速度が落ち、成長が停滞する
なぜテストを実施しない場合と同じ結果になるのか?
- テストが間違った理由で頻繁に失敗する
- 例 : プロダクション・コードはあっているが、テスト結果はエラーになっている
- バグを正しく検出できていない
- 例 : テストの実施範囲が、プロダクション・コードを網羅しきれていない
- テストに時間がかかりすぎて簡単に保守できなくなる
- 例 : テスト結果を確認できるまでの時間が長い
単体テストは、単に書くだけではその目標 (持続可能なプロダクトの成長) を達成できない。
1-4. 単体テストの質をどう判断するか
網羅率 (covarage) により、質の低いテストを判断する
コード網羅率 (実行されたコードの行数 / 総行数
)
テスト・スイートがプログラムのどの程度の部分をカバーしているかを測定する指標
例 : 偶数かを判定する関数とテスト・コード
function isEven($num) {
if ($num % 2 === 0) {
return true;
} else {
return false;
}
}
public function testIsEven()
{
$num = 2;
$result = isEven($num);
$this->assertTrue($result);
}
プロダクション・コードは全体で7行
、実際に通るのは3行
なので、
コード網羅率は3 / 7
(43%
) になる
- コードの書き方次第でコード網羅率は変動する
- ライブラリのコードは計測の対象外になる
コード網羅率は、簡単に変動する
そのため、コード網羅率が高いことをテストの質が高いことの指標にはできない
分岐網羅率 (経由された分岐の数 / 分岐の総数
)
テスト・コードの分岐経路のうち、テスト・スイートでどの程度網羅されているか
上の例の場合、
プロダクション・コードの分岐数は2つ
で、テスト・スイートで通るのは1つ
なので
分岐網羅率は、 1 / 2
(50%
) になる
分岐網羅率は、コード網羅率よりも正確な結果を得られる
分岐網羅率が低いことは、テストの質が低いことの指標にできる
網羅率が低いことは、テストの質が低いことの指標にできる
しかし、網羅率が高いことは、テストの質が高いことの指標にはできない
テスト・スイートの質はどのようにして判断するのか
- 自動的に評価する方法は存在しない
- 結局、個人的な判断に基づくしかない
1-5. 優れたテスト・スイートの特徴
- テストすることが開発サイクルの中に組み込まれている
- コードに変更を加えるたびに、テストが実施される
- コードの特に重要な部分だけがテストになっている
- 重要な部分 = アプリケーションの核となる部分 = ドメインモデル
- 重要でない部分
- インフラに関する部分
- 外部サービスに関する部分
- 構成要素同士を結びつける部分
- 最小限の保守コストで最大限の価値を生み出すようになっている
テストの労力を抑えつつ、テストから最大限の価値を引き出すために必要なこと
- 価値のあるテスト・ケースを認識できること (逆に、価値のないテスト・ケースも認識できること)
- 価値のあるテスト・ケースを作成できること
2. 単体テストとは何か?
単体テストの定義について
2-1. 単体テストとは何か
次の3つの性質をすべて備えているテストのこと
- 1単位の振る舞いを検証すること
- 実行時間が短いこと
- 他のテスト・ケースから隔離された状態で実行されること
「隔離」の意味をどう捉えるかで、古典学派とロンドン学派 (別名:モック主義者) の2つの派閥がある
アプローチ | 「単体」の定義 | テスト対象が依存する概念の扱い |
---|---|---|
ロンドン学派(モック主義者) | 1つのクラス | すべてテスト・ダブルに置き換えなくてはならない |
古典学派 | 1つのテスト・ケース | 他のテスト・ケースに影響を与える共有依存だけをテスト・ダブルに置き換える |
2-2. 「依存から隔離する」とはどういうことか
そもそも依存とは何か
あるコンポーネントAを動かすために、他のコンポーネントBが必要であること
参考 : https://zenn.dev/shun57/articles/1fd956346c4381#%E3%81%9D%E3%82%82%E3%81%9D%E3%82%82%E4%BE%9D%E5%AD%98%E3%81%A8%E3%81%AF%EF%BC%9F(%E3%82%8F%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84%E6%96%B9%E5%90%91%E3%81%91)
テスト対象の依存の例
テスト・ダブル
テストダブル (Test Double) とは、ソフトウェアテストにおいて、テスト対象が依存
しているコンポーネントを置き換える代用品のこと。ダブルは代役、影武者を意味する。
Wikipedia
- テスト・ダブルの種類に、モックやスタブなどがある
テスト・ダブルを使うと何がいいか
- テストを高速に実行できるようになる
- データベースなど外部リソースにアクセスする場合、通信するのに時間がかかる
- テストが常に同じ結果を返すようになる
- テストの結果が、外部リソースの状態に依存しなくなる
- テストの正確性が向上する
- テストで実行する箇所を隔離できる
- 原因の特定が高速にできる
- ロンドン学派では不変依存以外の全てのオブジェクトを隔離する (テスト対象を他の全ての依存から隔離する)
- 古典学派では共有依存のみをモックにする (テストケースを他のテストケースから隔離する)
依存の分類
種類 | 定義 | 例 |
---|---|---|
共有依存 (shered dependency) | テスト・ケース間で共有される依存 同じプロセス内で複数のテスト・ケースを実行した場合に結果に影響を与える要素のこと |
DB, DB操作するクラス, static関数・変数 |
プライベート依存 (private dependency) | テスト・ケース間で共有されない依存 | テスト対象が依存している他のクラス |
揮発性依存 (volatile dependency) | 呼び出すたびに異なる振る舞いを行う依存 | 現在日時, 環境変数 |
依存をテスト・ダブルに置き換える際の判断基準
- 依存するコンポーネントの状態が可変かどうか
- 不変なコンポーネントは置き換える必要はない
ロンドン学派のアプローチでは、不変依存以外はすべてテスト・ダブルに置き換える
不変依存の例
- 値オブジェクト
古典学派のアプローチでは、共有依存だけをテスト・ダブルに置き換える
共有依存の例
- DB
2-3. ロンドン学派のアプローチのメリット
- 細かい粒度での検証ができる
- 複雑な依存関係の検証を容易にできる
- テストが失敗した時に、原因がすぐにわかる
3. 単体テストの構造的解析
単体テストの構造など、単体テストに関する基本的なトピックについて
3-1 単体テストにおけるテスト・ケースの構造
単体テストのすべてのテスト・ケースは、準備 (Arrange)、実行(Act)、確認 (Assert)の3つのフェーズからなる。
(= AAAパターン)
AAAパターンを使うメリット
- すべてのテスト・ケースを統一感ある構造にできる
- 読みやすさが (可読性) が向上する
- テスト・スイート全体の保守コストが下がる
- 読みやすさが (可読性) が向上する
例 (https://docs.phpunit.de/en/10.0/writing-tests-for-phpunit.html)
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class GreeterTest extends TestCase
{
public function testGreetsWithName(): void
{
// Arrange
$greeter = new Greeter;
// Act
$greeting = $greeter->greet('Alice');
// Assert
$this->assertSame('Hello, Alice!', $greeting);
}
Arrange (準備)
- テスト・ケースの前提条件を満たすようにテスト対象システムとその依存の状態を設定する
Act (実行)
- テスト対象システムのメソッドを実行する
Assert (確認)
- テスト対象システムの振る舞いを検証する
- Actフェーズの結果が想定通りであることを確認する
- Assert の対象は、必ずしもメソッドの戻り値ではない
- 協力者オブジェクトの状態
- 協力者オブジェクトのメソッドが実行されること
3-2 単体テストにおいて回避すべきこと
- 同じフェーズが複数回登場する
- 例
- 準備 > 実行 > 確認 > 別の実行 > 別の確認
- なぜ良くないか
- 単体テストの定義 (1単位の振る舞いを検証すること) に反するため
- 対処法
- テスト・ケースを分割する
- 例
- if文を使うこと
- なぜ良くないか
- 単体テストの定義 (1単位の振る舞いを検証すること) に反するため
- 対処法
- テスト・ケースを分割する
- なぜ良くないか
3-3 AAAパターンの各フェーズを適切な大きさにするための手法
Arrangeフェーズが大きくなる場合
- 処理を共通化する
- Object Mather パターン
- Builder パターン
- privateメソッド・コンストラクタに切り出す
- テスト・ケース間の結合度が上がる・可読性が下がる、といったデメリットがあるため、上2つの方が良い
- 単体テストの定義 (他のテスト・ケースから隔離された状態で実行されること) に反する
- テスト・ケース間の結合度が上がる・可読性が下がる、といったデメリットがあるため、上2つの方が良い
Actフェーズが大きくなる場合
コードが正しく設計されている場合、Actフェーズは1行になる
1行になっていない場合、テスト対象の設計が悪い可能性がある
なぜか
- 一つの振る舞いが複数のメソッドで実装されているということは、どれか一つのメソッドの呼び出しを忘れた場合、正しい振る舞いをしなくなる (不変条件の侵害)
- 不変条件の侵害を防ぐための設計がカプセル化
- 不変条件とカプセル化についての参考記事 (https://qiita.com/Kokudori/items/2e4bd32abf7abea3186f)
Assertフェーズが大きくなる場合
- コードでうまく抽象化できていない
- Actフェーズでreturnされたオブジェクトの各フィールドを一つ一つ検証している場合など
- 実際のオブジェクトと期待値を表現したオブジェクトで検証するようにする
- Actフェーズでreturnされたオブジェクトの各フィールドを一つ一つ検証している場合など
3-4 テスト・メソッドに対する名前の付け方に関するベストプラクティス
読みやすいテスト・メソッド名を付けるための指針
- 厳格な命名規則をつけない
- 特に、テスト対象のメソッド名をテスト・メソッドの名前に含めるのはアンチ・パターン
- 単体テスト ≠ コードのテスト
- 単体テスト = アプリケーションの振る舞いのテスト
- 特に、テスト対象のメソッド名をテスト・メソッドの名前に含めるのはアンチ・パターン
- 問題領域に精通している非開発者にも伝わる名前にする
- アンダースコア (_) を使って名前を区切るようにする(英語の場合)
3.5 テスト・コードのリファクタリングパターン
パラメータ化テストへのリファクタリング (パラメータ化テスト、パラメタライズドテスト)
引数をさまざまな値に切り替えてテストしたい場合に有効。
パラメータ化テストは、さまざまなテスト・フレームワークでサポートされている。
PHPUnitでのパラメータ化テストの実装例 (https://zenn.dev/d_pontaro/articles/4f82e7b4ac149d69a61b)
パラメータ化テストのメリット
- コード量が減る
- 特に引数のバリエーションが多い場合に有効
パラメータ化テストのデメリット
- 検証するパラメータの数が増えた時に、コードが読みづらくなる
- テストが失敗した際に、どのパターンで失敗したのかわかりずらい
テスト・メソッド名のリファクタリング
テスト・メソッドの名前を自然な文章にする
自然な文章 = {主語} {動詞} {目的語}
(英語の場合)
テスト・メソッド名を自然な文章にすることのメリット
- 何を確認しようとしているかを理解しやすくなる