昔教えたもらった話。
テストケースを作るときに、かけ算になっている組み合わせを探して、足し算になるように直すとテストパターンを減らすことができます。
例。かけ算になっている組み合わせ。
以下の機能を持つ関数があるとします。(ちょっと無理矢理っぽいですが。)
- 入力値a,bを受け取る。
- 入力値aを3で割ったあまり(a'とする)を取得し、
- bが"+"なら、10 + a' を返す。
- bが"-"なら、10 - a' を返す。
- bが"*"なら、10 * a' を返す。
実装はこんな感じ。
public static int func( int a, String b ) { if ( "+".equals(b) ) { return 10 + (a % 3); } else if ( "-".equals(b) ) { return 10 - (a % 3); } else if ( "*".equals(b) ) { return 10 * (a % 3); } throw new IllegalArgumentException(); }
これのテストケースは次のようになります。
// func()のテスト。 assertEquals( Functions.func( 3, "+" ), 10 ); assertEquals( Functions.func( 4, "+" ), 11 ); assertEquals( Functions.func( 5, "+" ), 12 ); assertEquals( Functions.func( 3, "-" ), 10 ); assertEquals( Functions.func( 4, "-" ), 9 ); assertEquals( Functions.func( 5, "-" ), 8 ); assertEquals( Functions.func( 3, "*" ), 0 ); assertEquals( Functions.func( 4, "*" ), 10 ); assertEquals( Functions.func( 5, "*" ), 20 ); try { Functions.func( 3, "hoge" ); fail(); } catch ( IllegalArgumentException e ) {} try { Functions.func( 4, "hoge" ); fail(); } catch ( IllegalArgumentException e ) {} try { Functions.func( 5, "hoge" ); fail(); } catch ( IllegalArgumentException e ) {}
aはa'が変化するパターンすべてを網羅、bは取り得るパターンをすべて試すとして、3x4=12パターンのテストが必要になります。これが「かけ算の組み合わせ」です。
足し算にする。
足し算にするとは、一言で言うと「機能を分割して個別に動作を保証することで、結合テストでのテストパターンを最小にする」ことです。具体例として、上の例を足し算にしてみます。
まず、機能分割です。上の関数では「aを3で割ったあまりを得る」機能と「bに応じて計算する」機能の両方が含まれています。これをそれぞれ別の関数「funcA,funcB」とし、それを結合する関数として、「func2」を定義します。「func2」の仕様はもともとの「func」と同じです。
public static int func2( int a, String b ) { return funcB( funcA(a), b ); } public static int funcA( int a ) { return a % 3; } public static int funcB( int i, String b ) { if ( "+".equals(b) ) { return 10 + i; } else if ( "-".equals(b) ) { return 10 - i; } else if ( "*".equals(b) ) { return 10 * i; } throw new IllegalArgumentException(); }
作成したfunc2のテストは次の通り。分割した関数ごとにテストを行います。
- funcAのテストでは、数字を渡し、3で割ったあまりが返されることをテストする。
- funcBのテストでは、数字と文字列を渡し、文字列に応じて期待通りの結果が得られることを確認する。
- func2のテストでは、funcAとfuncBが連携して動作していることを確認する。funcA,Bの動作はそれぞれのテストで確認済みであるので、funcAの実行結果がfuncBの引数として渡っていることを、1パターンだけ試して確認すればOK。
// funcAのテスト // 数字を渡し、3で割ったあまりが返されることをテストする。 assertEquals( Functions.funcA( 3 ), 0 ); assertEquals( Functions.funcA( 4 ), 1 ); assertEquals( Functions.funcA( 5 ), 2 ); // funcBのテスト // 数字と文字列を渡し、文字列に応じて期待通りの結果が得られることを確認する。 assertEquals( Functions.funcB( 1, "+" ), 11 ); assertEquals( Functions.funcB( 1, "-" ), 9 ); assertEquals( Functions.funcB( 1, "*" ), 10 ); try { Functions.funcB( 1, "hoge" ); fail(); } catch ( IllegalArgumentException e ) {} // func2のテスト。 // funcAとfuncBが連携して動作していることを確認する。 assertEquals( Functions.funcB( 1, "+" ), 11 );
これにより、テストパターンは 3+4+1=8で済みます。本当は「aを3で割ったあまりを得る」機能のテストでは0とか6とかも試した方がよいのでテストケースを追加するとすると、「func」の場合は(3+2)*4=24パターンになりますが、「func2」だと(3+2)+4+1=10で済みます。足し算にすることで組み合わせの爆発を回避できるわけです。