無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

テストパターンを減らすTips

昔教えたもらった話。
テストケースを作るときに、かけ算になっている組み合わせを探して、足し算になるように直すとテストパターンを減らすことができます。

例。かけ算になっている組み合わせ。

以下の機能を持つ関数があるとします。(ちょっと無理矢理っぽいですが。)

  • 入力値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で済みます。足し算にすることで組み合わせの爆発を回避できるわけです。

まとめ

以上、機能は適切に分割して効率よくテストしよう、という話でした。結合テストは大事ですが、それだけだと往々にしてかけ算の組み合わせになって、パターンが増大してしまいます。そして結果的に抜けが生じて網羅性が低下する。それを補完するために、機能(クラス)分割と単体テストが重要になると考えます。

というわけで、テストケースがなんか長いな、と思ったら黄色信号です。1つの関数やクラスに機能が集中している可能性が高い。テストをさぼるためにも、クラス設計をちょっと見直してみた方がいいかもしれません。といって、あんまり別けすぎるのもアレだけど。