2013年6月17日月曜日

パラメータテスト TheoriesとFixture(後編)

株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。

前回からJUnitのパラメータ網羅テストを行うのに便利な「Theoriesアサーション」についてご紹介しています。

しかしこの「Theoriesアサーション」、前回に記載した通り、これ単品では余り便利ではありません。
一連のJUnit記事で紹介してきた「Encrosed」「カスタムMacher」、そして標準のJavaテクニック、これらを複合させて使い、初めてその真価を発揮するものなのです。

では、今回は「商品テーブルに対し、商品名の前方一致検索を行う」という、
どんなWebシステムにでも応用が利きそうなシチュエーションで行ってみましょう。

このメソッドに対し、テストパターンは以下です。

  • 商品名が完全一致するような文字列で検索して、一件の結果を取得する。
  • 商品名が前方一致するような文字列で検索して、二件の結果を取得する。
  • 商品名が後方一致するような文字列で検索して、結果を取得出来ない。

もちろん件数だけ数えるテストではありません。
きっちり「商品名」「商品コード」「値段」など、全フィールドが正しいことを確認します。

となれば、
「え? これってもう、普通にテストソースを3コ作るしか無いんじゃない?」
と思う方もいらっしゃるかもしれませんが、そんなことはありません。

今までご紹介してきたスキルを総動員すればスッキリと解決出来るのです。

@RunWith(Enclosed.class)
public class ShohinServiceTest {

 @RunWith(Theories.class)
 public static class 商品名前方一致検索パラメータテスト {

  @DataPoint
  public static ShohinFixture 検索結果無し = new ShohinFixture("無し無し",
    new ShohinSearchExpected() {
     public List<Shohin> getExpected() {
      return null;
     }
    });

  @DataPoint
  public static ShohinFixture 検索結果一件 = new ShohinFixture("ジェニシス石鹸",
    new ShohinSearchExpected() {
     public List<Shohin> getExpected() {
      List<Shohin> list = new ArrayList<Shohin>();
      Shohin s1 = new Shohin();
      s1.setShohinCode(1);
      s1.setShohinName("ジェニシス石鹸");
      s1.setShohinPrice(100);
      list.add(s1);
      return list;
     }
    });

  @DataPoint
  public static ShohinFixture 検索結果二件 = new ShohinFixture("ジェニ",
    new ShohinSearchExpected() {
     public List<Shohin> getExpected() {
      List<Shohin> list = new ArrayList<Shohin>();
      Shohin s1 = new Shohin();
      s1.setShohinCode(1);
      s1.setShohinName("ジェニシス石鹸");
      s1.setShohinPrice(100);
      list.add(s1);
      Shohin s2 = new Shohin();
      s2.setShohinCode(2);
      s2.setShohinName("ジェニシスシャンプー");
      s2.setShohinPrice(200);
      list.add(s2);
      return list;
     }
    });

  /**
   * 商品Fixture
   *
   */
  private static class ShohinFixture {

   /** 検索条件の商品名 */
   public String shohinName;

   /** 検索結果 */
   public ShohinSearchExpected expected;

   public ShohinFixture(String shohinName,
     ShohinSearchExpected expected) {
    this.shohinName = shohinName;
    this.expected = expected;
   }

  }

  /**
   * 商品検索結果インターフェース
   *
   */
  private interface ShohinSearchExpected {
   public List<Shohin> getExpected();
  }

  @Theory
  public void パラメータテスト(ShohinFixture fixture) {

   ShohinService shohinService = new ShohinService();
   List<Shohin> actual = shohinService.findByShohinName(fixture.shohinName);
   List<Shohin> expected = fixture.expected.getExpected();

   if(expected == null){
    assertThat(actual, is(nullValue()));
   }else{
    assertThat(actual, is(new ShohinListMacher(expected)));
   }

  }

 }

}

これはちょっと強烈なので、分解してご説明しましょう。

テストメソッド本体

まずこのテストコードはテスト本体が一つしかありません。

@Theory
public void パラメータテスト(ShohinFixture fixture) {

 ShohinService shohinService = new ShohinService();
 List<Shohin> actual = shohinService.findByShohinName(fixture.shohinName);
 List<Shohin> expected = fixture.expected.getExpected();

 if(expected == null){
  assertThat(actual, is(nullValue()));
 }else{
  assertThat(actual, is(new ShohinListMacher(expected)));
 }

}

3パターンあるテストもこれ一発で全部対応しています。
ソースの共通化や再利用は基本中の基本。これが「Theoryアサーション」のパワーです。
(なお、「標準マッチャーのnullValue()」と「カスタムマッチャー」も使っています。
これらについて忘れた方は昔の記事をご参照下さい。)
ところでこのメソッド、引数で「ShohinFixture」を受け取っていますね。
これが今回紹介するテクニックの中核です。

フィクスチャ

ここからがポイントです。
「DataPointアサーション」を定義することでフィールドにパターンを定義出来ることは前回の記事の通りです。
しかし、前回は「検索条件はString型一コ、結果はtrueで固定」という単純なものでしたが、これでは今回のようなバリエーションに富んだテストにた対応出来ません。
こういう場合は、テストパラメータと予測結果をパッキングした専用のクラスを作るのです。
このような役割を持つクラスのことを「フィクスチャ」と呼びます。(別にJUnitの機能というわけではなく、普通のJavaクラスです。)

/**
 * 商品Fixture
 *
 */
private static class ShohinFixture {

 /** 検索条件の商品名 */
 public String shohinName;

 /** 検索結果 */
 public ShohinSearchExpected expected;

 public ShohinFixture(String shohinName,
   ShohinSearchExpected expected) {
  this.shohinName = shohinName;
  this.expected = expected;
 }

}

今回はブログ記事として一括表示する都合上、内部クラスとして定義しましたが、本番では別途「fixture」というパッケージを作って外出しにした方がいいでしょう。
似たようなテストパターンでソース共有が出来ますし、ソースの見通しも良くなります。

しかし、コンストラクタで渡している検索条件の「String shohinName」はいいとして、検索結果である「ShohinSearchExpected expected」とは一体何でしょう?
以下に続きます。

パターン定義と無名クラス

テストメソッド本体は書きました。
フィクスチャも作りました。
後は「DataPointアサーション」を使って、検索条件と予想結果を専念すればOKです。

@DataPoint
public static ShohinFixture 検索結果一件 = new ShohinFixture("ジェニシス石鹸",
  new ShohinSearchExpected() {
   public List<Shohin> getExpected() {
    List<Shohin> list = new ArrayList<Shohin>();
    Shohin s1 = new Shohin();
    s1.setShohinCode(1);
    s1.setShohinName("ジェニシス石鹸");
    s1.setShohinPrice(100);
    list.add(s1);
    return list;
   }
  });

「ん? な、何か変だぞ!?」
と思われた方もいるかもしれません。
先に定義したフィクスチャにコンストラクタで値を渡しています。
引数の1コ目は普通にString型ですが、2コ目は……????

実はこの「ShohinSearchExpected」というのはインターフェースでして、
Javaではこのようにインターフェースを実現するクラス本体の無い、インターフェース単品でインスタンスを作ることも出来るのです。
これを「無名クラス」と言います。

「無名クラス」は使い方を間違えるとソースの可読性が下がってしまうので要注意ですが、
このテストケースのように、ローカルでちょっとクラスが欲しいケースには最適と言えるでしょう。

これによって、今回のテストパターンのように複数行に渡ってデータを定義しなければならないようなケースでも、フィールド一個の中に全て梱包出来るのです。

まとめ

如何でしたでしょう?
JUnitのパラメータ網羅テストは「似たようなソースのコピペを減らし、テストパターンの定義に専念出来る」かどうかが、品質を高める上でのキーとなります。

今回の記事は今までご紹介した機能を総動員したヘビーな内容でしたが、
最終的には「@DataPoint1コで1テストパターン。テスト本体は同一」という、テスト毎の独立性とソース共有を両立した理想的なテストソースを作ることが出来ました。

今回お見せした例は「検索条件がString一コ、結果がリスト」というものですが、それ以外のケースでも「検索条件が引数2つならフィクスチャのコンストラクタで渡す引数を増やす」「検索条件が複雑なら無名クラス用インターフェースを作る」など、応用で何とでもなります。プログラマーの腕の見せ所です。

今回のサンプルソースは「商品名検索3パターン」という小さなものであるため、わざわざフィクスチャやインターフェースを作らねばならない労力が大変という印象が先立ってしまうかもしれません。
しかし、このような高機能型JUnitは、プロジェクトの規模が大きくなる程に効果が大きくなります。
大規模ソース、大量テストパターンでキッチリこの書き方でJUnitを作れば、それはもう美しいの一言に尽きるソースになります。
ソースの再利用によって開発速度も上がって良いことばかりです。

おわりに

JUnitの機能は「Ignore」「Suite」「Exception取得」など他にもあるのですが、
単体開発者が押さえておかねばならない要点という意味では、ひとまずこんな所ではないかと思います。

そこで、ここから先はJUnit標準ではなく、その拡張プラグイン関連のご紹介にシフトしていこうと思います。

次回より新章開幕、「DBUnit編」です。

0 件のコメント:

コメントを投稿