開発日報

窓際エンジニアの開発備忘。日報は嘘です。

【忘備】グーグルのソフトウェアエンジニアリング ~ ユニットテスト ③ -明確なテストを書く- ~

明確なテストを書く

まず、初めに申し上げておきたいのはテストの失敗は良いことである。

なぜなら、失敗したテストは、エンジニアに有用なシグナルを提供し、ユニットテストが価値を提供する方法のうち主要なものだからである。

テストの失敗理由

テストの失敗理由は大きく以下の2つ

  • テスト対象システムに問題があるか、システムが不完全である。テストは本来このことを検証するために書かれる。
  • テスト自体に欠陥がある。失敗しているテストが既存のテストであればそのテストが脆いことを意味する。

原因究明の容易さはテストの「明確性」に依存する

  • 明確なテスト:テストの失敗理由と存在理由がエンジニアから見て明確なテスト。
    • 特に、当該機能及びテストの実装に関わっていないエンジニアにとってもわかりやすいこと。

明確なテストには、、以下の利点もある。

  1. テスト対象システムをドキュメント化する
  2. 新しいテストの基礎の役目果たす

以下、簡潔なテストを書くための原則を紹介する。

テストは完全かつ簡潔にせよ

  • 完全なテスト:そのテストがどのようにその結果に到達するのか理解するために必要な全情報を含んでいる。
  • 簡潔なテスト:他の紛らわしいか無関係な情報を含んでいない。

上記二点の性質は「コード共有」をめぐる考え方にも関連している。

特に、より明確なテストはDRY原則に反していることが多い。(なぜだろう?各テスト毎に必要な情報を全て含ませる、、定義するから?)

まとめると、テスト本体は重要でない情報や紛らわしい情報を全く含まずに、テストを理解するのに必要な情報をすべて含むべきである。

例)完全で簡潔なテスト

@Test
public void shouldPerformAddition() {
  Calculator calculator = new Calculator();
  int result = calculator.calculate(new Calculation(2, Operation.PLUS, 3));
  assertThat(result).isEqualTo(5);
}

メソッドではなく挙動をテストせよ

メソッドごとにテストを書くと、、 メソッドが複雑になるにつれてテストも複雑になり、実際は何をやってるかわからなくなる。

故に、テストは挙動向けに書くべきである。

[Q.] なぜ挙動向けにテストを書くと、テストが明確になるのか?

  1. 頭の中で複雑な構文解析しなくてよくなる。
  2. 原因と結果が分かりやすい
  3. 各テストが説明的なのでどの機能がすでにテストされているかわかりやすい。

挙動を強調するようにテストを構成せよ

全ての挙動のテストは3つの部分に分けられる

  1. 前提条件(~という前提条件で)
  2. 動作定義(~の場合は)
  3. 結果検証(その場合は~になる)

例)うまく構成されたテスト

@Test
public void transferFundsShouldMoveMoneyBetweenAccounts() {
  // 1. 最初の残高が150ドルと20ドルの口座があるという前提条件で
  Account account1 = new AccountWithBalance(usd(150));
  Account account2 = new AccountWithBalance(usd(20));

  // 2. 第一の口座から第二の口座へ100ドルを送金する場合
  bank.transferFunds(account1, account2, usd(100));

  // 3. その場合は、細心の口座残高は送金結果を正しく反映すべきである
  assertThat(account1.getBalance()).isEqualTo(usd(50));
  assertThat(account1.getBalance()).isEqualTo(usd(120));
}

複数ステップのテストを書く場合は2. と 3. を交互に定義しても良い

ただし、うっかり複数の挙動を同時にテストしてないか注意する必要がある。

テストされる挙動にちなんでテストを命名せよ

  • (bad) メソッド志向のテスト:メソッドにちなんで命名される。
    • 「testUpdateBalance」みたいなテスト名
  • (good) 挙動駆動のテスト:挙動にちなんで命名。より柔軟。
    • テスト名称は失敗のリポート内で目に見える数少ない手がかりを提供する。
      • 故に、テスト名は冗長であってもそれが正当化される

優れたテスト名は、

  1. システムに対して行われる動作と期待結果の両方を表す
  2. テストの挙動を要約する

注意すべきは、テスト名称に「また(and)」という単語が出てきた場合、複数の挙動をテストして いる可能性があるためテストを分けた方がよいかもしれない

例)入れ子命名パターン

describe("情報", function(){
  describe("正の数に",  function() {
    var positiveNumber = 10;
    it ("別の正の数を乗ずると正の数になる", function() {
      expect(positiveNumber * 10).toBeGreaterThan(0);
    });
    it ("負の数を乗ずると負の数になる", function() {
      expect(positiveNumber * -10).toBeLessThan(0);
    });
  })
});

テストにロジックを入れるな

基本、各テストが取り扱ってよいのは「入力セット」だけ。

テストにロジックが入っていると、以下のような問題が発生する。

  • テストのロジックの正しさはどうやって検証・担保する?
  • テストのテスト書く・・・?

特に気を付けたいのは、期待結果を表現するため等にテスト内で文字列の連携とかやりがちだがこれはテスト内のロジックに該当する。

例)コードに潜むバグを隠すロジック

@Test
public void shouldNavigateToAlbumsPage() {
  String baseUrl = "http://photos.hogehoge.com/";
  Navigator nav = new Navigator(baseUrl);
  nav.goToAlbumPage();

  // baseUrlの末尾に/が入っていておかしくなるが気づきにくい。
  assertThat(nav.getCurrentUrl()).isEqualTo(baseUrl + "/albums"); 
}
@Test
public void shouldNavigateToPhotosPage() {
  Naigator nav = new Navigator("http://photos.hogehoge.com/");
  nav.goToPhotosPage();
  assertThat(nav.getCurrentUrl()).isEqualTo("http://photos.hogehoge.com//albums"); // お、、
}

明確な失敗メッセージをかけ

失敗メッセージは、テストの明確性の最後の要素  →テスト失敗時にエンジニアは何を見るか。

良いテストメッセージは、、

  • 期待結果が明確に表現されている
  • 実際の結果が明確に表現されている
  • 関連する原因が明確に表現されている

また、失敗メッセージは失敗しているテストの重要な情報を手動で特定することが可能であるべき。