2011年7月4日月曜日

JMockit

数日前のポストで、この2・3年に普及してきた、Java の Mocking / Isolation フレームワークに触れた。今日は、そのうちの一つ JMockit を使って、以下のお題について解を考えてみる。

クラス TestTarget があり、Collaborator オブジェクトへの関連と、メソッド methodA() を持っている。

public class TestTarget {
   private final Collaborator collaborator = new Collaborator();
   public String methodA(String pattern) {
      int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
      return String.format(pattern, collaborator.methodB(hour));
   }
}

methodA() では、Collaborator オブジェクトのメソッド methodB() を呼んでいるが、methodB() の振る舞いについて分かっている事は、24時間制の「時」を表す整数を与えると、その時刻を表す単語("morning"や"夕方"など)を返すと言う事だけである。

さて、ここで methodA() における、Collaborator オブジェクトとの協調を含む、TestTarget の振る舞いをテストするコードはどのようなものになるか。

見ての通り、methodA() の振る舞いは、
  1. Calendar から現在時刻の「時」の部分を取得し
  2. これを Collaborator.methodB に渡して戻り値を受け取り
  3. 更にこれを書式化して返すというものになる。

テスティングポイントは以下のようになる。
  1. Calendar から、正しく「時」を取得している事
  2. collaborator に渡された「時」が、上記1で取得したものである事
  3. collaborator から返された文字列を正しく用いて、メソッドの戻り値を作っている事

また、以下のような事に留意する必要がある。
  • Collaborator.methodB() の現時点の挙動には依存しない事。
    時間の区切りがどうなっているか、言語が何であるかなどは、今後変わる可能性があるが、TestTarget クラスの責任範囲外である。
  • Collaborator.methodB() の実コードを呼ばない事。
    Collaborator は外部サービスが起動している事に依存していて、実行時間も無視できない。また TestTarget のテストで Collaborator コードまで呼ばれてしまうと、コード・カバレッジが実際を正しく反映しなくなると言う問題もある。
  • このテストをどの時間帯に実行しても結果が変わらない事。
    つまり、Calendar が返す時刻に依存してはいけない。

この UnitTest を、従来の dynamic proxy ベースの ツール(jmock など)を使ったり、ましてや状態ベースの普通の JUnitテストで書こうとすると、難しいテストコーディングになったと思う。

JMockit を使うと以下のような感じになる。
public class TestTargetTest {
   @NonStrict Collaborator collaborator;
   @Mocked({"getInstance", "get(int)"}) Calendar mockCalendar;
   @Test public void methodA() {
      final int HOUR = 7; 
      new Expectations() {{
         collaborator.methodB(HOUR); result = "morning";
         Calendar.getInstance(); result = mockCalendar;
         mockCalendar.get(Calendar.HOUR_OF_DAY); result = HOUR;
      }};
      String actual = new TestTarget().methodA("good %s!");
      Assert.assertEquals("good morning", actual);
   }
}
コードの意味は以下のようなもの
  • Calendar.getInstance() が モックの Calendar を返すように、getInstance() コードを差し替えた
  • Calendar のモックは、今、何時なのか尋ねられる事を期待し、またその際、固定値7 を返すよう記述した。
  • Collaborator のモックが、固定値7 で methodB()が呼ばれることを期待し、その際、固定値"morning"を返すよう記述した
  • 上記設定で、TestTarget.methodA()に"good %s!"が与えられ、"good morning"が得られる事を検証するよう記述した。


一度グリーンにしてから、いろいろコードを変えてみて期待通りにレッドになることを観察してみた。

せいぜい jmock の延長くらいかと思っていたら、使用感はかなり異なっていて、若干戸惑う。ただし、状態変化ベースだけではなく、相互作用ベースの UnitTest まで理解していれば、それほど高いハードルではない。DynamicProxy 系のテストを書いていていろいろ悩んだり困った経験のある人ほど、理解が早いと思う。

0 件のコメント:

コメントを投稿