2011年7月11日月曜日

JMockit と Swing と無名クラス

JMockit を使った Swing の UnitTest を考えてみる。今日はとりあえず、最初の一歩。
参考資料はこれ → Behavior-based testing with JMockit

====
まずこんなコードから始めてみる。Swing に限らず、GUI フレークワークの入門書の最初に出てくるような、何もないウィンドウを単に開いてみるだけのもの。
public class Exercise1 {
  public static void main(String[] args) {
     JFrame frame = new JFrame();
     frame.setSize(500, 300);
     frame.setVisible(true);
  }
}

要するに 500×300のウィンドウがビジブルになれば良い訳で、以下のようなテストコードになる。
public class Exercise1Test {
  @Test public void main() {
     new Expectations() {
        @NonStrict JFrame frame; {
        frame.setSize(500, 300);times=1;
        frame.setVisible(true);times=1;
     }};
     Exercise1.main(new String[]{});
  }
}

ここで、ウィンドウのクローズイベントに対応するアプリケーション終了処理が無い事に気づき、例えば、以下のように追加したとする。
public static void main(String[] args) {
  JFrame frame = new JFrame();
  frame.addWindowListener(new WindowAdapter() {
     @Override public void windowClosing(WindowEvent e) {
        System.exit(0);
     }
  });
  frame.setSize(500, 300);
  frame.setVisible(true);
}
この無名クラスを含むコードをどうやってテストするか。

以下のような方針をとってみた。
  • System クラスをパーシャルモックして、exit(0) 呼び出しを expect する(変な日本語だが…)。
  • 無名インナークラスについては、次のようにする。
    • addWindowListener に渡された、WindowListener を保持しておく。
    • main() 実行の後で、保持しておいた WindowListener のwindowClosing()を明示的に呼び出す。

コードはこんな感じ。
public class Exercise1Test2 {
  @Mocked({"exit"}) System system;
 
  static class WindowListenerCapturer implements Delegate {
     WindowListener captured;
     void addWindowListener(WindowListener l) {
        captured = l;
     };
  }
 
  @Test
  public void main() {
     final WindowListenerCapturer delegate = new WindowListenerCapturer();
     new Expectations() {
        @NonStrict JFrame frame; {
        frame.addWindowListener((WindowListener)any); result = delegate;
        frame.setSize(500, 300);times=1;
        frame.setVisible(true);times=1;
     }};
     new Expectations() {{
        System.exit(0);
     }};
     Exercise1.main(new String[]{});
     delegate.captured.windowClosing(null);
  }
}

ここでは、実際の実行パスの流れと同じになるように、main() からの Expectation と、windowClosing() からの Expectation を分けて書いてみた。

WindowListenerCapturer を使わないで、result = new Delegate() { ... }として、「...」のところで、windowClosing() を呼ぶやり方も試してみたが、何故か Eclipse プラグインがテスト終了を認識しない。

まあ上で示したコードでも、キャプチャのためのコードが若干増えることになるとはいえ、実行時の流れを表していると言う点では、却って分かりやすいような気もする。

2011年7月7日木曜日

Effective Java から例外関連項目

以下、『Effective Java (2nd Edition)』 から、9章「例外」に関係する項目。

:推奨 :注意 :禁止

Item 57: Use exceptions only for exceptional conditions
ループとかに使うなという基本中の基本。パフォーマンスにも悪影響あり。

Item 58: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors
まあ一応、教科書的な原則としてはこんな感じだろう。ただチェック例外の是非については、結構物議を醸す。そもそも、こんなの必要なのかと。Spring なんかも RuntimeException 派だし、C#や C++ でチェック例外が無いからといって困った事も全然ない。

Item 59: Avoid unnecessary use of checked exceptions
むやみにチェック例外を使うなと。例えば、「とりあえず呼んでみて例外が上がったらハンドリングする」なんてやり方より、「呼ぶ前にまずチェックする」やり方の方が、よりインテンショナルなコードになる場合がある。そんなメソッドのシグネーチャに例外を含めたって、誰の得にもならず負担が増えるだけ。

しかしここでも、いっそチェック例外なんて止めりゃいいじゃんなんて思いが、やはり沸いてきたりする…

Item 60: Favor the use of standard exceptions
例外に限らず全ての Java 標準APIに言えるけど、よく調べ、よく理解して、使い倒していくと、いろんな意味で効率的。低リスクで高い生産性が得られる。

Item 61: Throw exceptions appropriate to the abstraction
よくAPI なんかについて、低レベルとか高レベルとか言うけど、例外についてもレベルに合わせて使い分けよう、必要なら変換もしようという話。

Item 62: Document all exceptions thrown by each method
メソッドのドキュメントで、例外の説明をちゃんとしとけと。特に、RuntimeException だけで押し通す方針を選択した場合、チェック例外の面倒くささから解放される分、このドキュメント化だけはちゃんとしないと単なる手抜きになる。

Item 63: Include failure-capture information in detail messages
せっかく例外投げるんだから、ちゃんと原因究明の手がかりも含めようという、まっとうな話。まあ、面倒くさいし省かれがちだけど、これが正論。

Item 64: Strive for failure atomicity
例外送出による実行中断のおかげで、システムの状態が中途半端になったりしないように、例外発生時には呼び出し前の状態を復元しようという項目。

こういうのを考えると、注意喚起力が強いチェック例外って、やっぱアリかなあなんて思えてくる。少なくともドキュメントには、やはり可能性のある例外を列挙しておきたい。まあキャッチしたとしても、状態の復元はいつも簡単には行かないだろうけど。

Item 65: Don't ignore exceptions
これについては、前に書いた。驚くべき事に、例外を基底クラスでキャッチしてモミ消した挙句、鼻高々という面白い人種が、業界の底辺には多数棲息しているが、これらのおバカさん達についてもレポートした。

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 系のテストを書いていていろいろ悩んだり困った経験のある人ほど、理解が早いと思う。