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編」です。

2013年6月10日月曜日

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

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

現在はテスト自動化シリーズと題しましてJUnit関連の紹介を連載中です。

さて、JUnitの開発では、「渡す引数が違うだけで同じメソッドを何度もテストする」というパラメータテストを実施することがあります。
例えば、「文字列の半角英数字チェック」のメソッドをテストしようとすれば、引数として渡すテスト対象文字列は以下のように色々なパターンが存在します。

  • 半角英字
  • 半角数字
  • 記号
  • 全角英字
  • 全角数字
  • ひらがな
  • 空白スペース
  • 以下続く……

これらのパターンに対してそれぞれテストを実行し、全て正常に期待した結果になるかどうかをチェックするJUnitソースを作成するというシチュエーションです。

では、まずはダメなパターンから行ってみましょう。

@Test
public void 半角英数字チェックのテスト() {
    assertThat(TheoriesSample.isHankaku("a"),is(true));
    assertThat(TheoriesSample.isHankaku("1"),is(true));
    assertThat(TheoriesSample.isHankaku(","),is(false));
    assertThat(TheoriesSample.isHankaku("A"),is(false));
    assertThat(TheoriesSample.isHankaku("あ"),is(false));
    assertThat(TheoriesSample.isHankaku("*"),is(false));
    assertThat(TheoriesSample.isHankaku(""),is(true));
    assertThat(TheoriesSample.isHankaku(null),is(true));
    assertThat(TheoriesSample.isHankaku(" "),is(true));
}

こういう書き方はJUnitの作法としてNGです。
この場合、一つのテストメソッドで複数パターンのテストを実施していることになってしまいます。

「一つのメソッドで一つのテストパターン」

これがJUnitの作法です。
みんなが正しく作法を守ることで可読性の高い良いソースが生まれるのです。
面倒でもテストパターン毎に別々のメソッドにして下さい。

その作法を守ったパターンが以下になります。

@RunWith(Enclosed.class)
public class TheoriesSampleTest {

    public static class 半角英数字チェックのテスト {

        @Test
        public void 半角英数字チェック_半角英字はtrue() {
            assertThat(TheoriesSample.isHankaku("a"),is(true));
        }

        @Test
        public void 半角英数字チェック_半角数字はtrue() {
            assertThat(TheoriesSample.isHankaku("1"),is(true));
        }

        @Test
        public void 半角英数字チェック_記号はfalse() {
            assertThat(TheoriesSample.isHankaku(","),is(true));
        }

        //以下続く
    }

}
これなら「一つのメソッドで一つのテストパターン」のルールを守れます。

前回の記事で紹介した「Enclosedアサーション」を使えば「半角英数字チェックのテスト」でグループ化出来るので、メソッドが増えてもソースの見通しは比較的良い状態を保てます。

とはいえ、いくら正しい書き方と言っても、これは面倒でしょう。
「引数が1コ違うだけで他は全部同じなんだから、引数以外は共通化したい」と思うのがプログラマー精神です。

そこでご紹介するのが、今回記事のテーマ「Theoriesアサーション」です。
この機能は、上記のように「テストを実行するメソッド部分は同じだが、渡すパラメータだけは変えたい」という要望に対応するものです。

まずは以下にサンプルを記載します。

@RunWith(Enclosed.class)
public class TheoriesSampleTest {

    @RunWith(Theories.class)
    public static class 半角英数字チェックのテスト_true系 {

        @DataPoint
        public static String 半角英字 = "a";
        @DataPoint
        public static String 半角数字 = "1";
        @DataPoint
        public static String 空白 = "";
        //省略

        @Theory
        public void 半角英数字チェックパラメータテスト(String str) {
            assertThat(TheoriesSample.isHankaku(str),is(true));
        }

    }

    @RunWith(Theories.class)
    public static class 半角英数字チェックのテスト_false系 {

        @DataPoint
        public static String 記号 = ",";
        @DataPoint
        public static String 全角英字 = "A";
        @DataPoint
        public static String ひらがな = "あ";
        //省略

        @Theory
        public void 半角英数字チェックパラメータテスト(String str) {
            assertThat(TheoriesSample.isHankaku(str),is(false));
        }

    }
}

ここで新登場のアサーションは三つです。

  • @RunWith(Theories.class):このテストクラスがTheoryによる繰り返しであることを示す
  • @DataPoint:繰り返し時に渡すパラメータ
  • @Theory:テストメソッド本体

今まで引数として渡していたパラメータは「@DataPointアサーション」をつけたフィールド変数として定義します。
そして、そのクラスに「@RunWith(Theories.class)」を、テストメソッドに「@Theory」を付与することで、定義した全ての@DataPointがテストメソッドに繰り返し渡されるという構造です。

つまり、「@DataPoint」の定義だけをペタペタと増やしていけば、大量にあるテストパターンのパラメータも全て網羅出来るわけです。

この結果がエラーになった場合は、以下のように表示されます。


なるほど。
普通のテストケースだと「assertThat」の箇所が表示されますが、こちらの場合はエラーになったパラメータが表示されるわけです。

つまり、テストメソッドの中にassertThatが複数あった場合、その中のどのassertThatがエラーになったかは一目では分からないということになります。
このため、テストメソッド本体の中に記述するassertThatは一行一発で済ませるのが良いわけです。

このような場合には、過去に紹介した「カスタムMatcher」を作ることで解決して下さい。今まで紹介したJUnitの機能を組み合わせることで、スタイリッシュなJUnitソースを開発出来るのです。

次回予告

今回の記事ではTheoriesアサーションの簡単な例を紹介しました。
しかし、

  • 「@DataPointでテストメソッドに渡している引数がString型1コしか無いけど、同時に複数個渡したい場合はどうすればいいの?」
  • 「期待する結果がtrue,falseの2パターンだけならこれでいいけど、引数毎に違う場合はどうするんだ?」

という疑問が沸いた方もいらっしゃるかと思います。

そう、上記の例では、テストメソッドに渡す引数は1コ限定、「assertThat」の期待値部分もtrue/falseの固定値と、柔軟性が無いのです。
プロジェクトの都合に合わせて柔軟に対応するにはもう一息、JUnitの機能と言うより、書き方のテクニックが必要になります。
次回はその書き方のテクニック「Fixture」についてご説明します。

2013年6月3日月曜日

テストケースをグループ化 Enclosedアノテーション

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

現在は「テスト自動化シリーズ」と題してJUnitについて紹介中です。
 さて、今までの私の記事に「コードを綺麗に」「効率的に」「コピペしない」などの文言が随所に出てきていることで、薄々感づいていらっしゃるかもしれません。
JUnit開発経験がある方なら、多くの方が身に覚えがあるでしょう。

JUnit開発は、ソースを綺麗に書くことの戦いである。

JUnit開発は、それ自体は決して難しいものではありません。しかし煩雑な作業ではあるのです。
 しかもテスト系ですので、メインソースと比べて重要度が低く、品質管理が甘くなりがち。
この結果、JUnitソースはどうしても可読性の低いものになりやすいのです。

今回の記事は、そんな問題に対して役に立つ便利機能をご紹介しましょう。
それが、テストソースのグループ化機能「@RunWith(Enclosed.class)」です。

実例紹介 @RunWith(Enclosed.class)

JUnitのテストケースの数が増えてくると、「テストケースを性質毎に分別して、グループ化出来ないかな?」と思うようになります。
どのように分類するかはプログラマーの裁量次第ですが、例としては以下です。

正常/異常によるグループ分け

  • 処理が最後まで正常完了するテスト
  • 処理が途中でエラーになるテスト

DBのセットアップによるグループ分け

  • DBが空の状態からのテスト
  • DBにレコードが1件だけある状態からのテスト
  • DBにレコードが大量に登録された状態からのテスト

一つのクラスにテストケースを全部並列にズラッと書いた場合、「あのテストケースはどこに書いたっけ?」と探そうと思っても、パッと見で似たようなソースばかりなので視線が滑り、目的のソースが見つからないということになりがちです。
しかし、こうしてグループ化することで見易く整理されます。
では、実例をお見せしましょう。

/**
 * @author 技術開発事業部 遠藤 太志郎
 *
 */
@RunWith(Enclosed.class)
public class EnclosedSampleTest {

 public static class テーブルが空の状態から始まるテスト {

  /**
   * テーブルを初期状態にセットアップする。
   */
  @Before
  public void setUp()  {
   //テーブルを空にする処理
  }

  @Test
  public void test_レコードの新規登録_最小() {
   //テストコードを書く
  }

  @Test
  public void test_レコードの新規登録_フル桁() {
   //テストコードを書く
  }

 }

 public static class テーブルにレコードが登録状態からのテスト {

  /**
   * テーブルを初期状態にセットアップする。
   */
  @Before
  public void setUp()  {
   //テーブルにレコードをセットアップする処理
  }

  @Test
  public void test_レコードの更新_主キーで更新() {
   //テストコードを書く
  }

  @Test
  public void test_レコードの更新_フラグが一致するものを更新() {
   //テストコードを書く
  }

 }

}

EnclosedSampleTestクラスに「@RunWith(Enclosed.class)」というアノテーションが付与されています。
このアノテーションが「EnclosedSampleTestはJUnitテストソース本体ではなく、複数のテストクラスを取りまとめ役のクラスである。」という目印となるわけです。
そして、テストソース本体は配下のローカルクラスが担当します。
EnclosedSampleTestクラスにペタペタと全テストケースを書いてしまうよりも分類分けがされたのでスッキリしましたね。

(余談ですが、Javaはクラス名、メソッド名、変数に日本語が使えます。本体ソースでは普通こんなことはしませんが、テストソースならば日本語で書いた方が分かりやすくなるのでオススメです)

さて、上記のサンプルソース。
実は単に見やすくなっただけではなくて、もう一つ、非常に重要な機能が隠されています。

それは、「@Before」が2回登場していることです。
このアノテーションは似たような機能がJUnit3の頃からある有名なものですので、特に説明するまでも無いでしょう。

  • @BeforeClass:テストクラスを実行する最初に一回だけ呼ばれる。
  • @Before:全てのテストメソッドを実行する前に毎回呼ばれる。
  • @After:全てのテストメソッドを実行した前に毎回呼ばれる。
  • @AfterClass:テストクラスを実行した最後に一回だけ呼ばれる。

この4種のアノテーションは、その性質上、1テストクラスにつき1つしか書くことが出来ません。

このため、「こっちのテストはテーブルが空の状態からスタートしたい」「こっちのテストはテーブルにレコードを入れた状態からスタートしたい」と、初期セットアップ条件が複数存在するテストクラスの場合、@Beforeメソッド1つではセットアップに対応出来ないのです。

この結果、「@Beforeの機能は使わず、全てのテストメソッドの頭の部分にそれぞれ初期セットアップ処理をベタ書きする」という実に愚直なやり方に走るプロジェクトが世の中に存在してしまうのです。

しかし、そんな愚直なやり方をしてしまうと、セットアップ処理とテスト本体が同じメソッドの中に書かれていることになりますので、可読性が下がってしまいます。

セットアップ処理とテスト本体を分離する。

これはJUnitソースの可読性を高める為の基本中の基本です。
この要望に対応出来る便利な機能、それが「Enclosedアノテーション」なのです。

終わりに

今回紹介した「@RunWith(Enclosed.class)」は、そんな派手な機能ではなく、テストソースを綺麗にするために便利なテクニックといった所でしょう。

次回も再び、テストケースをスッキリするための便利なテクニックを紹介したいと思います。