2011年8月14日日曜日

JMockit を使って簡単に「後回し」するやり方

二つのクラスがあって、片方がもう片方に依存しているとする。つまり、参照関係や使用関係がある。

両方とも開発対象の場合、常に「依存される」側からしか作ろうとしない/作れない開発者がいるけど、逆の方向、つまり「依存される」側を一旦後回しにして「依存する」側から作った方が見通しが良い場合が多い。

方法はいくつもあるが、今日は JMockit を使って実演してみる。

====
◆ 仕様
依存する側のクラス
クラス CuT:Collaborator を使うクラス。こいつから作る。
 関連オブジェクト:
  col1: Collaboratorのインスタンス
  col2: Collaboratorのインスタンス
 コンストラクタ:受け取った2つの整数によりcol1 と col2 を初期化する
 メソッド:
  int foo(int number):
   col1 と col2 の bar() に number を渡し、結果の合計を返す

依存される側のクラス
Collaborator:CuT に使われるクラス。後回しにする。
 コンストラクタ:受け取った整数を保持する
 メソッド:
  int bar(int number)
   コンストラクタで受け取った整数と引数 number を用いて、何か計算をして返す。


◆ コーディング
両クラスのガラだけ書いてみる。CuT から作ってく趣旨なので、テストクラス CuTTest も書き始める。
public class CuT {}

public class Collaborator {}
public class CuTTest {}

まず、コンストラクタのテストを CuTTest に追加する。CuT のコンストラクタで受け取った引数が、二つの Collaborator のコンストラクタに受け渡されれば良いので、次のようなテストメソッドを書く。
@Test public void $init(final Collaborator mock) {

new Expectations() {
{
new Collaborator(2);
new Collaborator(3);
}
};
new CuT(2, 3);
}

とりあえず、存在しないコンストラクタを呼んでいるのでコンパイルエラーになるから、これを解消する。

まず、Collaborator のコンストラクタだが、こいつは以下の様なコードで後回しにする。
public Collaborator(int i) {

throw new AssertionError();
}

次に、CuT のコンストラクタだが、まずモックがちゃんと効いているか確かめるために、以下のように空の実装を書いて、一度テストを実行してみる。
public CuT(int number1, int number2) {}
テスト実行すると、「コンストラクタが引数 2 で呼ばれていない」といった感じのエラーメッセージが出力されるので、今度は以下のような感じで、ちゃんと書く。
public class CuT {

final Collaborator col1;
final Collaborator col2;
public CuT(int number1, int number2) {
this.col1 = new Collaborator(number1);
this.col2 = new Collaborator(number2);
}
}
再度テスト実行すると、テスト成功。

続いて同様に、まずテストから foo() を書く。

テスティングポイントは、「foo() で受け取った引数を、col1, col2 の bar()に渡している事」と「col1, col2 の bar()の戻り値の合計を、foo() の戻り値とする」なので、以下のようなテストコードになる。
@Test public void foo(

@Mocked(capture=1) final Collaborator col1,
@Mocked(capture=1) final Collaborator col2) {
new Expectations() {{
col1.bar(7); result = 10;
col2.bar(7); result = 100;
}};
Assert.assertEquals(110, new CuT(2, 3).foo(7));
}
Collaborator#bar() の実装が無いので、コンストラクタ同様に AssertionError 送出のみの後回しコードを追加して、コンパイルエラーを解消する。
public int bar(int i) {

throw new AssertionError();
}

CuT#foo() は、とりあえずダミーの固定値を返すようにして、テストを実行してみる。
public int foo(int i) {

return -1;
}

実行すると「bar が引数7 で呼ばれていない」といったエラーメッセージが出力される。期待通りなので、おもむろに foo() の実装を本物コードに修正する。
public int foo(int i) {

return this.col1.bar(i) + this.col2.bar(i);
}
これで、テストがちゃんと通るようになる。

一段落ついたら、クラス CuT の事は忘れて、Collaborator 実装に集中して取りかかればいい。

◆ 補足
ちなみに、JMockit に不慣れで capture の意味が分からなければ、このサンプルの col1 と col2 の capture を変えてテスト実行すれば、capture がどういう働きを持つのか分かると思う(場合によっては Expectations を NonSrictExpectations に替える必要がある)。

例えば、col1 のcapture を2に変えると、foo()の戻り値は20になる。col2 をそのまま変えずに col1 の capture を 0 にすると、foo()の戻り値は100になり、col2 の capture を2にすると、foo()の戻り値は200になる。

テストメソッド CuTTest#foo() のパラメータの col1, col2 が、CuT のフィールドのcol1, col2 のどれに対応するかが、capture で制御されているのが分かると思う。

さらに、もう一つ追記。上の CuTTest#foo()は以下のように書き換えられる。
@Test public void foo_explicitVerification(

@Mocked(capture=1) final Collaborator col1,
@Mocked(capture=1) final Collaborator col2) {
new NonStrictExpectations() {{
col1.bar(anyInt); result = 10;
col2.bar(anyInt); result = 100;
}};
Assert.assertEquals(110, new CuT(2, 3).foo(7));
new Verifications() {{
col1.bar(7);
col2.bar(7);
}};
}

若干、冗長になるが、テストの都合で指定したい動作と、検証したい動作を分けて書いている。実際の業務ロジックなんかで複雑な相互作用がある場合など、Expectations に全部書いてしまうと何がテスティングポイントだったのか分かりにくくなる事がある(特に後付けでテストを書かざるを得ない状況などで)。そんなとき Verifications のブロックに本当に検証すべきだった事を書いておくと、テスティングポイントを見失わずにすむ。

ちなみに、Verifications には、FullVerifications, FullVerificationsInOrder, VerificationsInOrderといった派生クラスがあるから、適当に使い分けるべし。

0 件のコメント:

コメントを投稿