home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

技術/TDD/JavaにおけるUnitTest時のMockオブジェクトの導入手法 (v1)

技術/TDD/JavaにおけるUnitTest時のMockオブジェクトの導入手法 (v1)

技術 / TDD / JavaにおけるUnitTest時のMockオブジェクトの導入手法 (v1)
id: 448 所有者: msakamoto-sf    作成日: 2009-10-02 10:11:49
カテゴリ: Java TDD プログラミング 

JavaでJUnitを使った単体テストのコードを書く時、Mockオブジェクトを使いたい、という場合がある。
例えばテスト対象のインスタンスメソッドの中で、トランザクションやデータベースの接続クラスのインスタンスをnewしていたりする時、
単体テストコードを作る為にMockのトランザクション/DB接続クラスに差し替えたい、というケース。

次のIBM developerworks の記事では、テスト対象のメソッドのインターフェイスを変えずに内部だけをリファクタリングし、
Mockオブジェクトに差し替える手法が紹介されている。

以下に、大まかなリファクタリングの流れやポイントを抜き書きする。
なおMockオブジェクトとは何か、テストコードを書く時どう便利か、などについては他の記事・書籍を参照の事。

リファクタリングの流れ

まずリファクタリング前のテスト対象のコードを示す。

class Application {
  //...
  public void run() {
    View v = new View();
    v.display();
    //...
  }
  //...
}

Applicationクラスのインスタンスメソッドrun()をテストしたいが、ここでViewクラスをMockオブジェクトにしたい。

元記事で紹介している手法では、まずApplicationクラスの中でprotectedなfactoryメソッドを用意する。

class Application {
  //...
  public void run() {
    View v = createView();
    v.display();
    //...
  }
  protected View createView() {
    return new View();
  }
  //...
}

そしてテストコード側では、インナークラスを利用してその場でcreateView()メソッドをMockオブジェクトを返すようにしたApplicationクラスを作り、
それをテスト対象とする。

class ApplicationTest extends TestCase {
  // まずMockとなるViewクラスとそのインスタンスを用意しておく。
  MockView mockView = new MockView();
  // この例では単純に"display()メソッドが呼ばれたか?"だけを後でassert出来るようにしておく。
  private class MockView extends View
  {
    boolean isDisplayed = false;
    public void display() {
      isDisplayed = true;
    }
    public void validate() {
      assertTrue(isDisplayed);
    }
  }
 
  // 実際のテストコード
  public void testApplication {
    // ここで、createView()だけ差し替えたApplicationクラス(の派生クラス)を
    // インナークラスで作る。これによりMockのViewを使ってrun()が実行される。
    Application a = new Application() {
      protected View createView() {
        return mockView;
      }
    };
    a.run();
    mockView.validate();
  }
}

基本手順

先の例で示したリファクタリング作業を、言葉で説明すると次のようになる。
まず用語を整理しておく。

  1. テスト対象オブジェクト(target object)
  2. 関連オブジェクト(collaborator object) : テスト対象オブジェクトによりnewされたり、取得されるオブジェクト
  3. Mockオブジェクト : 関連オブジェクトの中で、Mockにする派生クラス or 実装クラスとそのインスタンス
  4. 特異オブジェクト(specialization object) : Mockオブジェクトを使うようにリファクタリングされたテスト対象オブジェクト

以上の用語を用い、手順を説明する。

  1. テスト対象オブジェクトの中でnewするかsetter/getterなどから取得される関連オブジェクトを洗い出しておく。
  2. Extract Methodを関連オブジェクトのnew/取得部分に適用し、factoryメソッドを作る。(Martin Fowlerの「Refactoring: Improving the Design of Existing Code (Hardcover)」 110p 参照)
  3. factoryメソッドについては、テスト対象オブジェクトとそのサブクラスからアクセス可能にしておく。Javaであればprotectedメソッドにしておく。
  4. テストコードの中で、関連オブジェクトと同じインターフェイスのMockオブジェクトを作る。
  5. テストコードの中で、テスト対象オブジェクトの派生クラスである特異オブジェクトを作る。
  6. 特異オブジェクトの中で、テストを通せるよう、factoryメソッドを上書きしてMockオブジェクトを返すようにする。
  7. (オプション) 元々のテスト対象オブジェクトのfactoryメソッドが、Mockオブジェクトではなく本来の関連オブジェクトを返す事をテストする。

リファクタリング例

銀行のATM装置のテストを書く場面をサンプルとして、リファクタリングをしてみる。
Mockオブジェクトを導入する前の単体テストコードを以下に示す。

// 引き落としの確認
public void testCheckingWithdrawal() {
  // 開始時点での残高
  float startingBalance = balanceForTestCheckingAccount();
 
  AtmGui atm = new AtmGui();
  insertCardAndInputPin(atm);
 
  atm.pressButton("Withdraw"); // 引き落とし
  atm.pressButton("Checking");
  atm.pressButtons("1", "0", "0", "0", "0");
  assertContains("$100.00", atm.getDisplayContents());
  atm.pressButton("Continue");
 
  assertEquals(startingBalance - 100, balanceForTestCheckingAccount());
}

引き落としに対応するAtmGuiクラスのコードは以下のようなものとする。

private Status doWithdrawal(Account account, float amount) {
  Transaction transaction = new Transaction();
  transaction.setSourceAccount(account);
  transaction.setDestAccount(myCashAccount());
  transaction.setAmount(amount);
  transaction.process();
  if (transaction.successful()) {
    dispense(amount);
  }
  return transaction.getStatus();
}

このやり方は動く事は動く。しかしチェック対象のアカウントの残高が直接マイナスされるため、この単体テストの前後のテストコードに影響する怖れがある。
対処法としては、例えばテストコード中に前提条件に必要な残高の値をチェックしたり、あるいは補正するコードを実装する手法があるが、テストや実装コードが複雑になる。
ここではMockオブジェクトを導入するリファクタリングにより、実際のTransactionクラスではなくMockのTransactionを使うようにしてみる。

まず関連オブジェクトはTransactionなので、それのfactoryメソッド(protected)を導入する。

private Status doWithdrawal(Account account, float amount) {
  Transaction transaction = createTransaction(); // "new Transaction()"から修正
  transaction.setSourceAccount(account);
  transaction.setDestAccount(myCashAccount());
  transaction.setAmount(amount);
  transaction.process();
  if (transaction.successful()) {
    dispense(amount);
  }
  return transaction.getStatus();
}
 
// 追加したfactoryメソッド
protected Transaction createTransaction() {
  return new Transaction();
}

テストクラスの方で、MockTransactionクラスをメンバクラスとして定義する。

private MockTransaction extends Transaction {
 
  private boolean processCalled = false;
 
  // 実際の処理が行われないよう、process()メソッドをオーバーライドしておく。
  public void process() {
    processCalled = true;
    setStatus(Status.SUCCESS);
  }
 
  // Mockが正常に呼ばれたかassertするために使う
  public void validate() {
    assertTrue(processCalled);
  }
}

最後に、MockTransactionクラスを使うようにした特異オブジェクトをテストコードの中で作成する。

public void testCheckingWithdrawal() {
 
  mockTransaction = new MockTransaction();
 
  // Mockを使うようにした特異オブジェクトを作成
  AtmGui atm = new AtmGui() {
      protected Transaction createTransaction() {
        return mockTransaction;
      }
  };
 
  insertCardAndInputPin(atm);
 
  atm.pressButton("Withdraw");
  atm.pressButton("Checking");
  atm.pressButtons("1", "0", "0", "0", "0");
  assertContains("$100.00", atm.getDisplayContents());
  atm.pressButton("Continue");
 
  assertEquals(100.00, mockTransaction.getAmount());
  assertEquals(TEST_CHECKING_ACCOUNT, mockTransaction.getSourceAccount());
  assertEquals(TEST_CASH_ACCOUNT, mockTransaction.getDestAccount());
  mockTransaction.validate();
}

こうすることで、残高の前提条件を気にする事は無くなり、Transactionオブジェクトに渡した値が正常である事をもって、引き落とし処理の確認と出来る。
実際に前後の残高をチェックするのは、AtmGuiクラスとTransactionクラスの結合した時の挙動に関わってくる為、結合テストでチェックする事としておく。
この例では単体テストの意味を、あくまでもテスト対象のオブジェクトのI/Oのみをチェックするものとしている。
ここでのテスト対象はAtmGuiであるので、AtmGuiがTransactionオブジェクトと予期したとおりにコミュニケーションできていれば問題ないものとしている。

インナークラスの使用について

ATMの例ではAtmGuiのcreateTransactionメソッドのみをインナークラスで派生させオーバーライドしている。今回は1メソッドをオーバーライドすることで簡単に対処できた。
他のテストで別のメソッドをオーバーライドしたい場合などは、インナークラスではなく普通のクラスとしてまとめた方がよいかも知れない。

もう一つ、インナークラスを使った理由としてはMockオブジェクトを簡単に共有できるからという理由がある。
インナークラス内からは、その外部の変数にアクセスできる。そのため、テストコード中でnewしたMockTransactionインスタンスをそのまま、Mockのfactoryメソッドが返すようになっている。

インナークラスからその外部へのアクセスについては、マルチスレッドや再入問題(re-entrant)が絡む場合は注意する事(synchronizedで保護するなど)。

本当はMockではなくTransactionを使っているか?のテスト

上のATMの例では、実際のコードではMockTransactionではないホンモノのTransactionを使っているか?という点をテスト出来ていない。
これをテストするには、下記のようにcreateTransaction()メソッドの戻り値のクラスをチェックする。

AtmGui atm = new AtmGui();
Transaction t = atm.createTransaction();
assertTrue(!(t instanceof MockTransaction));

このテストを通す為には以下の条件が満たされている必要がある。

  1. createTransaction()がprotectedであること。
  2. テストクラスがAtmGuiと同じpackageにあること。

なおAtmGuiとcreateTransaction()がpublicである場合は、テストクラスのpackageは何処でも構わない

ちなみに、以下のassertは今回は使えない。

assertTrue(t instanceof Transaction)

なぜならMockTransactionはTransactionの派生である為、MockTransactionオブジェクトが返された場合もこのassertは成立してしまうからである。

From factory method to abstract factory

元記事では、ここで"From factory method to abstract factory"というセクションがあり抽象ファクトリを導入してみてはどうか、という意見に対する反論が載せられている。
詳細は元記事を参照して貰うとして、こちらでは自分自身の意見をまとめてみる。(概要としては元記事とあまり変わらない・・・とは思ってます)

話題になっているのは、要するにテスト対象のインターフェイス(返値や引数)を変えてもっとテストしやすくしてはどうか、という事なのだけれど、それは主従が逆転してしまうのではないか。
テストの為にインターフェイスを変えるのはやりすぎだ、という事。
またそれに関連して、新しいクラス/インスタンスとのコミュニケーションを追加するのは、テストする時の相互作用を増やしてしまうので良くないのでは、と言う事。

自分の基本的な認識としては、「リファクタリング」は外部インターフェイスを変えずに内部を改修する為の手法であるため、インターフェイスを変えてしまうのはリファクタリングの範疇を超える。
今回のATMの例では、publicメソッドのインターフェイスは変えずに、内部でcreateTransaction()を用意する事で何とか対処できた。これはリファクタリングの範疇内であると考えている。

テストコードの複雑さについては、Mockオブジェクトを利用した場合も、利用せずに残高のチェックや補正用のコードを実装した場合も、あまり「複雑さ」という点では変わらないと思う。
むしろインナークラスが出てきたりするので、その辺を押さえたレベルのJavaプログラマでないとテストコードを理解したり、自分なりのテストを追加するのもままならない。

ただし、ここで注目すべきはむしろテストコードの「副作用」だと考えている。
残高チェックや補正用コードをテストの為に実装するのは、そもそも元のテストコードが副作用を持っていたからである。
テストコード間で副作用があるために、その副作用を抑える為にあれやこれやと「テスト用ユーティリティクラス/メソッド」が実装されていく事になる。
そのためにコード量が増えて複雑さが増したとして、それでも副作用自体は残っているわけであるから、いつかどこかで綻びが出てくる。
そしてその都度、あれやこれやとテスト間の影響をデバッグしたりして調査する事になる。

であるならば、複雑さ(コード量/Javaプログラマに要求される技量)が増えたとしても副作用それ自体を減少させる方向に向かった方が良い。
そのための手法の一つが、今回元記事で紹介されているリファクタリング例であると考える。

まぁ実際問題、テストコードというのは書くのが難しい・・・。YakiBikiでもそうだったけど。
どこまで単体テストの範疇にするのか、という問題もあるし。というかそこを間違えると、テストコード作成の難易度が一気に上下する。
とりあえず、今回の元記事で紹介されているリファクタリングの手法をストックしておき、どこかで使えると良いのだけれど・・・。


プレーンテキスト形式でダウンロード
表示中のバージョン : 1
現在のバージョン : 2
更新者: msakamoto-sf
更新日: 2009-10-03 13:33:10
md5:a544054ef3e0498e19a4fd1185451c40
sha1:35e5e1c6483e7e512c822382c207c7f08a38ff01
コメント
コメントを投稿するにはログインして下さい。