Open12

Study | 単体テストの考え方/使い方

TakayyzTakayyz

単体テストでは、1単位のコード(a unit of code)を検証するのではなく、1単位の振る舞い(a unit of behavior)を検証するようにします。
〜略〜
ここで意味する単体は複数のクラスにまたがることもあれば単一のクラスに収まることもあり、さらには、ちょっとしたメソッドの実装で終わることもあります。

2.3.1 TIP

「単体」の指す粒度や認識を統一することは大事。

TakayyzTakayyz

(複雑な依存のあるクラスをテストする際に)本来考えるべきことは膨大で複雑な依存関係を持つクラスを検証するための方法を見つけ出すことではなく、そのような複雑な依存関係を構築しなくても済むようにするための方法
〜略〜
テストの作成において、もし、準備(Arrange)フェーズがあまりにも大きくなるようであれば、何らかの設計の問題がそこにある可能性が高いのです。このような場合、モックを使って複雑な依存関係を隠しても、根本的な問題解決にはなりません。

2.3.2

テスト・ダブルを用いてテスト時に依存について考えなくてもよくするのではなく、そもそも設計を見直すことを検討する。

TakayyzTakayyz

2章まとめ

単体テストの定義

  • 1単位の振る舞い(a unit of behavior)を検証すること
  • 実行時間が短いこと
  • 他のテストケースから隔離された状態で実行されること
    • ロンドン学派:テスト対象となる単体(クラス)を他の単体から隔離すべきという考え方(=テスト・ダブルを多様することになる)
    • 古典学派:単体テストのテストケースをそれぞれ隔離しなくてはならないという考え方

テストが失敗した際に原因の特定が容易なのはロンドン学派であるが、コード変更毎にテストを実行するという前提があれば古典学派(1単位の振る舞いをテストするやり方)でもテストが失敗する直前に変更を加えたコードに問題があることが明確になる為、原因の特定が難しくなることはほとんどない。

TakayyzTakayyz

※前提としてAAAの構造を採用

もし、準備(Arrange)フェーズが他の2つのフェーズを合わせたサイズよりもはるかに大きくなるのであれば、その一部を同じテスト・クラスのプライベートなメソッドに切り出したり、その部分を作成する別のファクトリ・クラスを作ったりしたほうが良いでしょう。このような準備フェーズのコードをテスト・ケース間で共有することに関する有用なパターンとしてよく知られているものの中にオブジェクト・マザー(Object Mother) と呼ばれるパターンとテスト・データ・ビルダー(Test Data Builder) と呼ばれるパターンがあります。

3.1.4

TakayyzTakayyz

確認(Assert)フェーズで確認する項目はどのくらいあればよいのか?

単体テストにおける「単体」とは、1単位のコードではなく1単位の振る舞いのことです。1単位の振る舞いによって複数の結果が生じることはあり得ることであり、ひとつのテスト・ケースでその結果をすべて検証することは自然なことなのです。

3.1.5

基本的に1つの結果しかassertしないようにしていたから考え方が少し変わった。

TakayyzTakayyz

テストの可読性の為に。

  • もし、AAAパターンを採用したテスト・ケースにおいて、準備フェーズや確認フェーズのコードを書くのに空白行が不要なのであれば、各フェーズの頭に付けているコメントを取り除き、空白行で各フェーズを区切るようにする。
  • そうでない場合は、各フェーズの頭にそのフェーズを示すコメントをつけるようにする

3.1.8

test('準備フェーズ・確認フェーズに空白行不要なテスト', function () {
    $hoge = 1;
    $piyo = 'piyo';
    $sut = new Hoge();

    $actual = $sut->handle($hoge, $piyo);

    expect($actual)->toBe('expected');
});

test('準備フェーズ・確認フェーズに空白行必要なテスト', function () {
    // Arrange
    $hoge = 1;
    $hogehoge = 2;
    $piyo = 'piyo';

    $xxx = $hoge + $hogehoge;

    $sut = new Hoge();
    // Act
    $actual = $sut->handle($xxx, $piyo);
    // Assert
    expect($actual)->toBe('expected');
    expect($actual->zzz)->toBeTrue();

    expect($actual->yyy)->toBe('yyy');
});
TakayyzTakayyz

3章まとめ

  • テストの可読性のために
    • AAAパターンを適用すべき
    • テスト対象システムをsutと名付けるのがよい(SUT: System Under Test)
    • アサーションのメソッドを提供してくれるライブラリ等を適宜活用する
  • 1つの振る舞いを検証するにあたって1つの振る舞いには複数の結果が生じることはあるので、ひとつのテストケースでその結果をすべて検証することは自然なこと
  • 実行フェーズに記述するコードが1行を超す場合はテスト対象となるコードのAPI設計が甘い可能性がある
  • テストデータ(test fixture)をコンストラクタで準備するとテストケース間の結びつきが強くなってしまうため、ファクトリメソッドを用意する
TakayyzTakayyz

良い単体テストを構成する4つの柱

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

(4-1章より)

TakayyzTakayyz

コードは資産ではなく負債である。
→ コードベースが大きくなればより多くの潜在的バグを抱えることになる

TakayyzTakayyz

リファクタリングへの耐性

リファクタリングを行った際にテストが壊れにくいことを指す。
リファクタリングを行った結果 機能の振る舞い自体は変わらないのにテストが失敗する場合、このテストのことを 偽陽性(false positive) と呼ぶ。
偽陽性の発生が多いとテストの失敗に対する慣れが生じ、本当にバグが原因でテストが失敗しても見過ごすリスクが大きくなる。

偽陽性の原因

実装の詳細をテストしてしまっている為。(ex. SUTの中で「このメソッドが呼ばれたか」や「この順番で処理が実行されたか」等)
→最終的に振る舞いが変わらないのであれば詳細を意識する必要はない。

対応

実装の詳細をテストするのではなく、SUTから返される最終的な結果(振る舞い)をテストする。


ただし、メソッドに引数を追加するなどの変更を加えた結果テストが失敗するケースも一種の偽陽性だが、表示されるエラーに沿って引数を追加するだけでいいのでこの手の偽陽性は許容できる。(というか許容せざるを得ない)