開発日報

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

テストダブルの原則 ① ~概要と導入について~

テストダブルの原則

ユニットテストはコードが複雑になるに連れて、書くのが難しくなってくる。

また、本番のコードがいくつもの外部APIを呼び出していたり、その結果をDBに保存していたりする場合、それらの挙動を本番コードのみのユニットテストだけで再現・担保するのは難しいという課題がある。

上記の課題の解決のためにテストダブルという手法が用いられる。

テストダブルに影響されるソフトウェア開発の概念

  • テスト可能性:テストダブルを利用するにはまずコードベースが「テスト可能」になっているべき
    • つまりテストは本物の実装をテストダブルと取り換え可能になっているべき
  • 応用性:テストダブルを不適切に応用するとかえってテストが脆く、複雑になる
    • 状況によって本物の実装の利用を優先すべき時もある
  • 忠実性:テストダブルの挙動を置き換え対象の本物の実装の挙動とどれだけ近いところに似せられているか

テストダブルの利用と本物の実装の利用には往々にしてトレードオフが存在する。 状況やユースケースに応じて適切な方を選択すべき。

 → 迷ったらまずは本物の実装を利用してみた方がよい。

Googleでのテストダブル

テストダブルは適切に使用すれば生産性とソフトウェア品質に多くのメリットをもたらすが、府積雪に利用されるとデメリットも多い。

教訓

  • モッキングフレームワークを利用しすぎるのは危険。
    • バグの検出率が下がる
    • テストの保守が大変

結果として、モッキングフレームワークよりも実際の実装をテストに利用することをゆうせんするようになった

テストダブルの基本概念

テストダブルの例

例 )クレジットカード決済処理を必要とするeコマースサイトを想定したコード例、、

class PaymentProcessor {
  private CreditCardService creditCardService;
  ......
  boolean makePayment(CreditCard creditCard, Money amount) {
    if (creditCard.isExpired()) { return false; }
    boolean success = 
      creditCardService.chargeCreditCard(creditCard, amount);
    return success;
  }

このケースでは、テスト内で本物のクレジットカードサービスを利用することは不可能。

そのため、本物のシステムの挙動をシミュレーションするために、代わりにテストダブルを利用する

// 簡易的なテストダブル
class TestDoubleCreditCardService implements CreditCardService {
  @Override
  public boolean chargeCreditCard(CreditCard creditCard, Money ammount) {
    return true;
  }
}
// テストダブルの利用
@Test public void cardIsExpired_returnFalse() {
  boolean success = paymentProcessor.makePayment(EXPIRED_CARD, AMOUNT);
  assertThat(success).isFalse();
}

この例では、本番コードの挙動はクレジットカードサービスに依存していないため、クレジットカードの有効期限が切れた場合のテストを適切に行える。

シーム

  • テスト可能:コードは、そのコード向けにユニットテストを書けるような形式で書かれている場合に、テスト可能であるといえる。

  • シーム:テストダブルを利用できるようにすることで、コードをテスト可能とする手法

    • ディペンデンシーインジェクションは、シームを導入する一般的なテクニックである。
// ディペンデンシーインジェクション
class PaymentProsessor {
  private CreditCardService creditCardService;

  PaymentProsessor(CreditCardService creditCardService) {
    this.creditCardService = creditCardService;
  }
}
// テストダブルを渡す
PaymentProessor paymentProessor =
  new PaymentProcessor(new TestDoubleCreditCardService());

テスト可能なコードを書くには先行投資が必要。

テスト可能性は考慮があとになるほど、コードベースへの適用が難しくなる。

そのため、テスト可能性の実現は、特にコードベースの存続期間の初期に、決定的な重要性を持つ