2011年8月27日土曜日

Advanced Level テストマネージャ試験受けてみた

今日、JSTQB という団体がやってる、ソフトウェア・テスト技術者試験の Advanced Level 試験(Foundation Levelの上級試験)を受けてきた。

全65問のうち、きっと問題のミスだろうと思われる不可解なところを除けば、全問できたような気がする。まあ9割5分とか9割でも普通は合格だと思う。

というかあまり難しくなかった。去年、実施したらしいトライアル版試験の統計をみると合格率10%というから、さぞかし難関だろうと思ってたけど、試験時間を半分過ぎた辺りから余裕な感じで退出する人も多かったりして、今回は合格率50%を越えるような気がしないでもない。

ところで、試験勉強はISTQB(JSTQBの親の国際団体)が出している試験のシラバス(学習事項)をひたすら読むだけだけど、これがよくできていて勉強が苦にならない。

以前のポストで、Foundation Level のシラバスを紹介したけど、さすがにそれよりはテスター向けの専門性が高い。それでも自分のようなテスト専門じゃない開発者にとっても、読み応えがある。

実プロジェクトのテスト技術者の教科書としてもそのまま使えそうな内容で、暗記事項の羅列などはほとんど無いかわりに、なんというか現場感覚があるので、過去に携わったプロジェクトや今現在進行中のプロジェクトについて、いろいろ考えさせられる。

ちなみに Advanced Level 資格には テストマネージャ、テストアナリスト、テストテクニカルアナリストという種別があり(今回のはテストマネージャだった)、3つそろったら、なんか格上の称号が与えられるらしい。まあ、欲しくないと言えば嘘になる。

2011年8月15日月曜日

JMockit: 基底クラスのコンストラクタのすりかえ

基底クラスのコンストラクタ呼び出しだけをモックですりかえるにはどうすれば良いか。

以下のように実験してみた。AssertionError を投げているところを、設定ファイルの読み込みとかマスタデータへの依存とか、面倒なセットアップが必要だったり時間がかかったりする処理に読み替えると、ニーズが分かると思う。

====
例題 (1)
クラス BaseClass を継承したクラス CuT があるとする。こんな感じ。
public class CuT extends BaseClass {

public CuT() {
super();
}
}

ところが、BaseClass の実装はこんな感じだったとする。
public class BaseClass {

protected BaseClass() {
throw new AssertionError();
}
}

以下のテストコードを実行すると、当然、AssertionEror が発生してテストが落ちるが、落とさずにインスタンスを作るにはどうすれば良いか?
@Test public void testCuTConstructor() {

//ここに何を書けば落ちないか
new CuT();
}

答え (1)
以下の様にして、基底クラスのコンストラクタをすり替えられる。
@Test public void $init() {

new MockUp<BaseClass>() {
@Mock void $init() { /*とりあえず何もしない*/}
};
new CuT();
}


例題(2)
例題(1) にちょっと書き足してみる。
public class BaseClass {

protected final int bar;
protected BaseClass(int bar) {
this.bar = bar;
throw new AssertionError();
}
}
public class CuT extends BaseClass {
public CuT(int bar) {
super(bar);
}
public int foo() {
return this.bar * 100;
}
}
@Test public void $init2() {
new MockUp<BaseClass>() {
//ここに何も書かないと、下のアサーションが失敗する
};
Assert.assertEquals(200, new CuT(2).foo());
}
このままだと基底クラスの bar が 2 で初期化されず、foo() が 0 * 100 = 0を返してしまうので、アサーションが失敗する。MockUp の匿名インナー型定義に、何を書き足せばテストが通るか?


答え (2)
コンストラクタ実行中のタイミングで、モック対象インスタンスの bar フィールドに適当な値を設定すれば良いわけだが、そのインスタンスをどう取得するかというのが問題。"it"フィールドという仕組みを利用すれば良いらしい。
@Test public void $init2() {

new MockUp<BaseClass>() {
BaseClass it;
@Mock void $init(int bar) {
Deencapsulation.setField(it, bar);
}
};
Assert.assertEquals(200, new CuT(2).foo());
}
この例では、bar は private フィールドなのでDeencapsulation ユーティリティクラスを使っている。ちなみに bar は final だけど、問題なく設定できている。

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といった派生クラスがあるから、適当に使い分けるべし。