ラベル JUnit編 の投稿を表示しています。 すべての投稿を表示
ラベル JUnit編 の投稿を表示しています。 すべての投稿を表示

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)」は、そんな派手な機能ではなく、テストソースを綺麗にするために便利なテクニックといった所でしょう。

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

2013年5月27日月曜日

JUnit4 Matcherの使い方(後編)

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

前回の記事「JUnit4 Matcherの使い方(前編)」では、JUnitの基本機能であるMatcherの使い方について解説しました。
汎用的なチェックならば、その記事で紹介した既存Matcherの機能で対応出来るでしょう。

しかし、「汎用的チェックではなく、プロジェクト特有なチェックはどうすればいいの?」という疑問が残ったかと思います。

今回の記事「JUnit4 Matcherの使い方(後編)」では、そんなプロジェクト個別の要望にお応えする機能「カスタムMatcher」を紹介します。

さて、「カスタムMatcher」というのは、要するに自分でMatcherを作るという意味です。
「自分で作らなきゃならないのであれば意味無いじゃないか」と思われる方もいらっしゃるかもしれませんが、そんな事はありません。
確かに自分で作る手間こそ存在しますが、JUnit標準に従うことでソースの可読性が上がったり、ソースを再利用出来たりと、基本ですが非常に大事なメリットが得られるのです。

実例:シンプル編

では、例を挙げてご説明しましょう。
簡単な例として、「数字が偶数かチェック」という機能を実現してみます。

まず、この「偶数チェック」を既存Macherでチェックすると以下のようになります。

/**
 * 数字が偶数かチェック
 */
@Test
public void test偶数チェック() {

 int num = 10;

 assertThat(num % 2, comparesEqualTo(0));
}

この程度なら、別に何も難しいことはありません。
数字を2で割って、余りが0であれば偶数というロジックです。
このテストがこの1回限りであれば、これで済ませてもOKです。

しかし、プロジェクトの性質により「複数のテストケースで何度も偶数チェックを行わなければならない」という事情があるとすればどうでしょう?
あちこちに同じ「num % 2」のロジックをコピペして開発していくのでしょうか?

それはちょっとソースとして美しくありません。
そこで登場するのがカスタムMatcherです。

カスタムMatcherは「org.hamcrest.BaseMatcher」か「org.hamcrest.TypeSafeMatcher」を継承して拡張することで作成出来ます。

どちらを継承すれば良いのか、という点で悩みますが、BaseMatcherにタイプセーフ機能を追加して便利になっているのがTypeSafeMatcherですので、普通はこちらを使えば良いでしょう。

こうしてTypeSafeMatcherを継承して作ったカスタムMatcherがこちらです。

import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
/**
 * 数字が偶数であることをチェック
 *
 * @author 技術開発事業部 遠藤 太志郎
 *
 */
public class EvenNum extends TypeSafeMatcher<Integer> {

 @Override
 protected boolean matchesSafely(Integer item) {

  return item % 2 == 0;
 }

 @Override
 public void describeTo(Description description) {

  description.appendText("<偶数>");

 }

}

「matchesSafelyメソッド」の引数「item」にチェック対象値が入ってきますので、これに対しチェック条件を記述します。
この結果が「false」の時、describeToに進みます。
「descriveToメソッド」はエラー発生時の文言を設定する部分です。

これでカスタムMatcherが作成出来ました。
これを使ってテストメソッドを書くと以下になります。

/**
 * 数字が偶数かチェック
 */
@Test
public void test偶数チェック() {

 int num = 10;

 assertThat(num, is(new EvenNum()));
}

「数字を2で割って余りをチェック」というロジックをカスタムMatcher側に持たせることが出来たので、テストケースを作る人はただEvenNumを呼び出すだけで良くなりました。

このEvenNumがエラーになると以下のように文言が表示されます。


表示文言も自分でカスタマイズ出来るので分かりやすいですね。

実例:応用編


次に、これを使った応用編に行ってみましょう。「エンティティやDTOのチェック」です。

例えばデータベースから何かを検索した時の検索結果は、大抵の場合は「String型のパラメータ単体」ではなくて、複数のパラメータをフィールドに持つエンティティやDTOの形で取得されるものです。

例として、以下に商品エンティティを定義します。
特に何の特徴も無い、タダのデータのパッケージというだけのクラスです。

package jp.co.net.genesis.junit.sample.custommatcherテスト;

/**
 * 商品エンティティ
 *
 * @author 技術開発事業部 遠藤 太志郎
 *
 */
public class Shohin {

 /** 商品コード */
 private int shohinCode;

 /** 商品名 */
 private String shohinName;

 /** 価格 */
 private int shohinPrice;

 /**
  * 商品コードを取得します。
  * @return 商品コード
  */
 public int getShohinCode() {
     return shohinCode;
 }

 /**
  * 商品コードを設定します。
  * @param shohinCode 商品コード
  */
 public void setShohinCode(int shohinCode) {
     this.shohinCode = shohinCode;
 }

 /**
  * 商品名を取得します。
  * @return 商品名
  */
 public String getShohinName() {
     return shohinName;
 }

 /**
  * 商品名を設定します。
  * @param shohinName 商品名
  */
 public void setShohinName(String shohinName) {
     this.shohinName = shohinName;
 }

 /**
  * 価格を取得します。
  * @return 価格
  */
 public int getShohinPrice() {
     return shohinPrice;
 }

 /**
  * 価格を設定します。
  * @param shohinPrice 価格
  */
 public void setShohinPrice(int shohinPrice) {
     this.shohinPrice = shohinPrice;
 }

 /* (非 Javadoc)
  * @see java.lang.Object#toString()
  */
 public String toString() {
     StringBuilder bul = new StringBuilder();
     bul.append("商品コード=").append(shohinCode).append(",");
     bul.append("商品名=").append(shohinName).append(",");
     bul.append("価格=").append(shohinPrice);
     return bul.toString();
 }

}

商品テーブルを検索して、このエンティティで結果が取れてくると定義しまして、商品テーブルを主キーで検索した結果が正しいことをチェックするテストケースは、既存Matcherで実現するなら以下になります。

/**
 * 商品テーブルを主キーで検索する
 */
@Test
public void test_商品テーブルを主キーで検索する() {

 ShohinService service = new ShohinService();

 Shohin actual = service.findById(123);

 Shohin expected = new Shohin();
 expected.setShohinCode(123);
 expected.setShohinName("鉛筆");
 expected.setShohinPrice(105);

 assertThat(actual.getShohinCode(), is(expected.getShohinCode()));
 assertThat(actual.getShohinName(), is(expected.getShohinName()));
 assertThat(actual.getShohinPrice(), is(expected.getShohinPrice()));
}

このソースの問題は以下の部分です。
  • assertThat(actual.getShohinCode(), is(expected.getShohinCode()));
  • assertThat(actual.getShohinName(), is(expected.getShohinName()));
  • assertThat(actual.getShohinPrice(), is(expected.getShohinPrice()));
assertThat3連発!!

このテストケースは例として「主キー検索」を挙げています。
しかし、実際の開発では「商品名で検索」「商品名を前方一致で検索」「価格が一定数値以下で検索」など、その他色々な機能に対してテストする必要が出てきます。
それら全てに対して全部同じコピペを繰り返しては、実に泥臭いソースになってしまいます。

もちろん、今回で例にしている商品エンティティのフィールド数はたったの3つですけれども、実際の開発では10や20もフィールドがあるなんて普通のことです。
それをこんな風にコピペ連発で作っていたら、それだけでソースが埋まってしまいます。その中にコピペミスが紛れ込んでいる可能性も少なくありません。
汚いソースコードはバグの温床です。

ここはカスタムMatcherで綺麗にしましょう!!

/**
 * Shohinエンティティが一致していることをチェック
 *
 * @author 技術開発事業部 遠藤 太志郎
 *
 */
public class EqualToShohin extends TypeSafeMatcher {

 /** 期待値 */
 private Shohin expected;

 /** 異なる値 */
 private String difference;

 public EqualToShohin(Shohin expected){
  this.expected = expected;
 }

 @Override
 protected boolean matchesSafely(Shohin actual) {

  //商品コード一致チェック
  if(actual.getShohinCode() != expected.getShohinCode()){
   difference = "商品コード";
   return false;
  }

  //商品名一致チェック
  if(!actual.getShohinName().equals(expected.getShohinName())){
   difference = "商品名";
   return false;
  }

  //商品価格一致チェック
  if(actual.getShohinPrice() != expected.getShohinPrice()){
   difference = "商品価格";
   return false;
  }

  return true;
 }

 @Override
 public void describeTo(Description description) {

  description.appendValue(expected);
  description.appendText(difference).appendText("が異なっています。");

 }

}


処理の要点は以下です。

  1. コンストラクタで期待値となる商品エンティティを渡す。
  2. 「matchesSafely」で各カラムをチェックする。異なっている箇所がある場合はfalseを返し、全部正常であればtrueを返す。
  3. 「describeTo」でエラー時に表示するメッセージを形成する。オブジェクトのメッセージは、そのオブジェクトの「toString()」メソッドで表示されるため、予め商品エンティティでは「toString()」をオーバーライドして分かりやすい表示を作っておく。

これだけです。
特に難しいことはありません。

このカスタムMatcherでfalseになった場合、以下のようなメッセージが表示されます。


どこが異なっているのかも一目瞭然です。

/**
* 商品テーブルを主キーで検索する
*/
@Test
public void test_商品テーブルを主キーで検索する_EqualToShohin() {

 ShohinService service = new ShohinService();

 Shohin actual = service.findById(123);

 Shohin expected = new Shohin();
 expected.setShohinCode(123);
 expected.setShohinName("鉛筆");
 expected.setShohinPrice(104);

 assertThat(actual, is(new EqualToShohin(expected)));

}

無事に「assertThat」一行でチェック出来るようになりました。
やはりJavaのソースは、このようにオブジェクト単位で操作出来るのが美しいですね。

最初にカスタムMatcherを作るのが少々面倒だと感じられたかもしれませんが、この最初の一手間を惜しまずにキッチリやっておけば、以降のテストケース作成が非常に楽になります。

上流工程で頑張っておけば下流工程が楽になるのは、メインソースもテストソースも同じです。

JUnit開発は「所詮はテストソースだし」ということでメインソースよりも品質が劣悪になることが多いですが、それは自縄自縛というもの。
テストソースもメインソースと同じ。最初にクラス設計を頑張っておくことでソースが綺麗になり、品質が上がり、全体的に効率化されて、後々楽になってくるのです。

このカスタムMacherを駆使して、ぜひ綺麗で強固なシステムを作り上げて下さい。

2013年5月26日日曜日

JUnit4 Matcherの使い方(前編)

株式会社ジェニシス 技術開発事業部の遠藤 太志郎です。
ハンドルネームはTacyです。

現在、『テスト自動化シリーズ』を連載中。
前回に引き続きJUnit4の使い方をご説明します。

今回のテーマは「Matcher」です。
「Matcher」はJUnit4の注目機能というか、むしろベース部分と言って良いでしょう。
前回の記事で僅かに登場していますが、

 assertThat(sample001.getConnectString(str1, str2), is("株式会社ジェニシス技術開発事業部"));

の「is」の部分。これがMatcherです。
つまるところ、assertThatの一致条件を定義する機能ですね。

まず、以下の文章を記述する上の用語として以下を定義します。

実行結果:actual
予想結果:expected

さて、JUnitのテストというのは、「actural == expected」という完全一致を比較することが殆どです。
しかし、状況に依っては

  • actual != expected
  • actual < expected
  • actual == null
  • actualの頭の文字がexceptedかどうかチェック
  • actualがリストで、その中にexpectedが含まれているかどうかチェック。
  • actualが単一の値ではなくてDTOである。

というような色々と細かい要望も出てくるものです。
それをどうやってテストケースとして実現するか、というのもJUnitコーディングの腕の見せ所と言えるでしょう。

上のケース、実は全部自力でコーティングすれば何とかなるものです。
例えば一番上の「actual != expected」を実現したい場合、

assertThat(actual.equals(expected), is(false));

とでも書けば実現出来ます。
しかし、こうやって細かい条件を全部その場で自力で書いていたら作業効率が上がりません。プログラマーの個人差で実装方法が変わってソースの可読性が下がったり、バグが潜在したり、といったことも予想されるので、余り望ましいことではありません。
自力コーディングはやればやるほど品質が下がるものです。

JUnitでは、上記のような細かい要望に対応出来るように、前もって様々なチェック機能を備えていてくれています。
それが「Matcher」です。
Macherをフル活用し、自力コーディングではなく、JUnit推奨コーディングを行うことを目指しましょう!!

本稿「Matcherの使い方」では、基本となる既存Matcherを紹介する今回と、自分でMatcherを作ってしまうカスタムMatcherを紹介する次回の2回に渡り掲載します。

まずは紹介の前に環境のセットアップです。

セットアップ

公式サイトよりJUnitと拡張パッケージのhamcrestをダウンロードしてきます。
EclipseにはJunitライブラリを自動的にビルドパスに追加してくれる機能がついていますが、これだとライブラリが最新とは限りませんし、必要最低限しか追加されないので欲しい機能が使えないなど困ったことになりますので、手動でセットアップします。
(Eclipseには改善を期待したい所です。)
本稿を執筆する際の最新バージョンは、JUnitが4.11、hamcrestが1.3ですので、本稿もこれに準じて記載します。
ダウンロードしたら以下ファイルをビルドパスに追加します。
  • junit-4.11.jar
  • hamcrest-core-1.3.jar
  • hamcrest-library-1.3.jar
これにてセットアップは完了です。

既存Matcher紹介

まず基本的なMatcherとして、JUnitでは以下が用意されています。

  • org.hamcrest.CoreMatchers(hamcrest-core-1.3.jar)
  • org.junit.matchers.JUnitMatchers(junit-4.11.jar)

これら基本Matcherを拡張したものとして、以下クラスがあります。

  • org.hamcrest.Matchers(hamcrest-library-1.3.jar)

それぞれ入っている機能が違ったり、重複していたり、非推奨になっていたりします。
(特にJUnitMatchersは淘汰されて大半が非推奨になってしまいました)

基礎的な機能だけならばCoreMatchersのみで対応可能ですが、
Matchersを使うことでほぼフル機能が使えるようになります。
本稿ではMatchersをメインに検証しています。

次に、以下のようにクラスをstatic importして下さい。

 import static org.hamcrest.Matchers.*;

これで対象クラスのMatcherを使うことが出来るようになりました。

さて、肝心の各クラスが有している機能ですが、セットアップに記載しているURLの先ではJavaDocもダウンロード出来ます。
詳細まで詳しく知りたい方はこちらを参照して頂くとしまして、現場ベースの本ブログとしては、やはり現場プログラマーが一番知りたい「どうやれば何を実現出来るんだ?」という一覧を記載したいと思います。

基本機能

クラス メソッド 概要
CoreMatchers
Matchers
is 対象オブジェクトが期待値と一致するかチェック
CoreMatchers
Matchers
equalTo 対象オブジェクトと期待値オブジェクトの一致チェック。オブジェクトのequalToメソッドで比較する。
Matchers hasToString 対象オブジェクトをtoStringした結果が期待文字列と一致するかチェック
CoreMatchers
Matchers
not macherの非定型を作成する。

複数の期待条件を組み合わせる機能

クラス メソッド 概要
CoreMatchers
Matchers
allOf 対象オブジェクトを複数のMacherでチェックして、全てがtrueであるかどうかをチェック
CoreMatchers
Matchers
anyOf 対象オブジェクトを複数のMacherでチェックして、どれか一つでもtrueであるかどうかをチェック
CoreMatchers
Matchers
both 対象オブジェクトを二つのMacherでチェックして、両方共にtrueかどうかチェック
CoreMatchers
Matchers
either 対象オブジェクトを二つのMacherでチェックして、どちらか一方でもtrueかどうかチェック
Matchers isIn 対象オブジェクトが期待するCollectionオブジェクトの中に含まれているかどうかをチェック。期待値にCollectionをセットする。
Matchers isOneOf 対象オブジェクトが期待するCollectionオブジェクトの中に含まれているかどうかをチェック。期待値に可変長引数をセットする。

文字列の検証に使用する機能

クラス メソッド 概要
Matchers isEmptyString 対象文字列が空白かどうかチェック。nullはNG
Matchers isEmptyOrNullString 対象文字列が空白、もしくはnullかどうかチェック
CoreMatchers
Matchers
startsWith 対象文字列の先頭が期待値で始まっているかをチェック
CoreMatchers
Matchers
endsWith 対象文字列の末尾が期待値文字列であるかどうかをチェック
CoreMatchers
Matchers
containsString 対象文字列に期待文字列が含まれているかをチェック
Matchers equalToIgnoringCase 対象文字列と期待値文字列の一致チェック。大文字と小文字を区別しない。
Matchers equalToIgnoringWhiteSpace 対象文字列と期待値文字列の一致チェック。空白スペースがある場合に空白スペースの数を意識しない。
Matchers samePropertyValuesAs 対象文字列に対し、期待値の文字が順番で含まれているかをチェック
Matchers stringContainsInOrder 対象文字列に対し、期待値の文字が順番で含まれているかをチェック

数値の検証に利用する機能

クラス メソッド 概要
Matchers comparesEqualTo 対象数値が期待数値と一致しているかチェック。isよりもメッセージが詳細
Matchers greaterThan 対象数値は期待数値より大きい(actual > expected)かどうかをチェック
Matchers greaterThanOrEqualTo 対象数値は期待数値より以上(actual >= expected)かどうかをチェック
Matchers lessThan 対象数値は期待数値未満(actual < expected)かどうかをチェック
Matchers lessThanOrEqualTo 対象数値は期待数値以下(actual <= expected)かどうかをチェック
Matchers closeTo 対象数値が期待される基準値から±指定値の範囲内に収まっているかをチェック

nullチェック系(文字列のチェックについては上記「文字列の検証に使用する機能」参照)

クラス メソッド 概要
CoreMatchers
Matchers
notNullValue 対象オブジェクトがnullでないことをチェック
CoreMatchers
Matchers
nullValue 対象オブジェクトがnullであることをチェック

Listや配列など、チェック対象値が複数の場合に使用する機能

クラス メソッド 概要
Matchers array 対象配列オブジェクトの要素それぞれに対して、期待値の条件を満たすかどうかをチェック
Matchers arrayContaining 対象オブジェクトのそれぞれに対して期待値と一致するかチェック。順番の整合性もチェック
Matchers arrayContainingInAnyOrder 対象配列オブジェクトのそれぞれに対して期待値と一致するかチェック。順番の整合性はチェックしない。
Matchers arrayWithSize 対象配列オブジェクトのサイズが期待値と一致するかチェック
Matchers contains 対象オブジェクトのそれぞれに対して期待値と一致するかチェック。順番の整合性もチェック
Matchers containsInAnyOrder 対象オブジェクトのそれぞれに対して期待値と一致するかチェック。順番の整合性はチェックしない
Matchers empty 対象Colelctionオブジェクトが空かどうかをチェック
Matchers emptyArray 対象配列が空オブジェクトかどうかをチェック
Matchers emptyCollectionOf 対象配列オブジェクトが空かどうかをチェック。型のチェックも行う。
Matchers emptyIterable 対象Iterableオブジェクトが空かどうかをチェック
Matchers emptyIterableOf 対象Iterableオブジェクトが空かどうかをチェック。型のチェックも行う。
CoreMatchers
Matchers
everyItem 対象オブジェクトの全ての要素に対して期待値を満たすかどうかをチェック
CoreMatchers
Matchers
hasItem 対象Iterableオブジェクトの中に期待値を満たすオブジェクトが含まれているかどうかをチェック
Matchers hasItemInArray 対象配列オブジェクトの中に期待値を満たすオブジェクトが含まれているかどうかをチェック
Matchers hasItems 対象Iterableオブジェクトの中に期待値を満たすオブジェクトが含まれているかどうかをチェック。期待値には複数条件を設定出来る。
Matchers hasSize 対象Collectionオブジェクトのサイズチェック
Matchers iterableWithSize 対象Iterablesの型と要素数が期待値と一致するかをチェック

Mapのチェックに使用する機能

クラス メソッド 概要
Matchers hasEntry 対象Mapに期待のkey,valueが入っているかチェック
Matchers hasKey 対象Mapに期待されるkeyが入っているかをチェック
Matchers hasValue 対象Mapに期待されるvalueが入っているかチェック

インスタンスや型をチェックする系

クラス メソッド 概要
CoreMatchers
Matchers
any 対象オブジェクトが期待するクラスのインスタンスであるかをチェック。instanceOfと同じ
CoreMatchers
Matchers
instanceOf 対象オブジェクトが期待するクラスのインスタンスであるかをチェック
CoreMatchers
Matchers
isA 対象オブジェクトのクラスが期待値のものであるかをチェック
CoreMatchers
Matchers
sameInstance 対象オブジェクトが期待するオブジェクトと同じインスタンスであることをチェック
CoreMatchers
Matchers
theInstance 対象オブジェクトが期待するクラスのインスタンスであるかをチェック
Matchers typeCompatibleWith 対象オブジェクトが期待するクラスを継承しているかチェック

その他

クラス メソッド 概要
CoreMatchers
Matchers
anything 常にtrueになる。このオブジェクトはチェックしなくて良いということを明示する為の機能
CoreMatchers
Matchers
describedAs エラー時に表示される文言を上書きする
Matchers eventFrom 対象イベントオブジェクトの発生元が期待値であるかをチェック
Matchers hasProperty 対象JavaBeansオブジェクトに期待する名称のプロパティが存在するかチェック
Matchers hasXPath 対象XMLドキュメントに期待するパスが存在するかをチェック

大量にありますね!!

表を見ると結構機能が被っていたり、他の機能で代行出来たりするものが多いです。
これは英語として綺麗になるように配慮されていることも理由の一つとしてあり、例えば「allOf」と「both」は似たような機能ですが、パラメータが3つ以上の時は「allOf」、2つの時は「both」を使うとソースの可読性が上がるわけです。

これらをフル活用して綺麗なテストコードを書き上げて下さい。


しかし、実際のプロジェクトではこのような汎用的なケースばかりではなくて、
プロジェクト独特のチェックを行いたい時もあります。

例えば「プロジェクトで作ったDTOの各パラメータが正しいかチェックしたい」といったケースです。

DTOの各パラメータが正しいかどうかは、各パラメータ全部に対して上記の既存Matcherを使ってチェックを行えば実現は可能ですが、それだとソースが冗長になってしまいます。

せっかくDTOとして一括りになっているのですから、「assertThat(dto1,is(dto2)」みたいに一行で完結させたいと思うのがJavaのオブジェクト指向というものです。

こういう場合は、自分でMatcherを作ってしまうことでテストソースを綺麗に出来るようになります。

次回、「Matcherの使い方(後編)」はカスタムMatcherをご紹介します。

追伸

上記の表を作る時に検証として書いたソースを以下に掲載しておきますので、こちらもお役に立てれば幸いです。

package jp.co.net.genesis.junit.sample;

import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.hamcrest.collection.IsIterableWithSize;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * JUnitのMacherを検証
 *
 * @author 技術開発事業部 遠藤 太志郎
 *
 */
public class MacherCheckTest {

 /**
  * 対象オブジェクトを複数のMacherでチェックして、全てがtrueであるかどうかをチェック
  */
 @Test
 public void testメソッドチェック_allOf() {

  assertThat("myValue", allOf(startsWith("my"), containsString("Val")));
 }

 /**
  * 対象オブジェクトが期待するクラスのインスタンスであるかをチェック。instanceOfと同じ
  */
 @Test
 public void testメソッドチェック_any() {

  assertThat(new MacherCheckTest(), any(MacherCheckTest.class));
 }

 /**
  * 対象オブジェクトを複数のMacherでチェックして、どれか一つでもtrueであるかどうかをチェック
  */
 @Test
 public void testメソッドチェック_anyOf() {

  assertThat("myValue", anyOf(startsWith("foo"), containsString("Val")));
 }

 /**
  * 常にtrueになる。このオブジェクトはチェックしなくて良いということを明示する為の機能
  */
 @Test
 public void testメソッドチェック_anything() {

  assertThat("myValue", anything(""));
 }

 /**
  * 対象配列オブジェクトの要素それぞれに対して、期待値の条件を満たすかどうかをチェック
  */
 @SuppressWarnings("unchecked")
 @Test
 public void testメソッドチェック_array() {

  assertThat(new Integer[]{1,2,3}, is(array(equalTo(1), equalTo(2), equalTo(3))));
 }

 /**
  * 対象オブジェクトのそれぞれに対して期待値と一致するかチェック。順番の整合性もチェック
  */
 @Test
 public void testメソッドチェック_arrayContaining() {

  assertThat(new String[]{"foo", "bar"}, arrayContaining("foo", "bar"));
 }

 /**
  * 対象配列オブジェクトのそれぞれに対して期待値と一致するかチェック。順番の整合性はチェックしない。
  */
 @Test
 public void testメソッドチェック_arrayContainingInAnyOrder() {

  assertThat(new String[]{"foo", "bar"}, arrayContainingInAnyOrder("bar", "foo"));
 }

 /**
  * 対象配列オブジェクトのサイズが期待値と一致するかチェック
  */
 @Test
 public void testメソッドチェック_arrayWithSize() {

  assertThat(new String[]{"foo", "bar"}, arrayWithSize(equalTo(2)));
 }

 /**
  * 対象オブジェクトを二つのMacherでチェックして、両方共にtrueかどうかチェック
  */
 @Test
 public void testメソッドチェック_both() {

  assertThat("fab", both(containsString("a")).and(containsString("b")));
 }

 /**
  * 対象数値が期待される基準値から±指定値の範囲内に収まっているかをチェック
  */
 @Test
 public void testメソッドチェック_closeTo() {

  assertThat(1.03, is(closeTo(1.0, 0.04)));
 }

 /**
  * 対象数値が期待数値と一致しているかチェック。isよりもメッセージが詳細
  */
 @Test
 public void testメソッドチェック_comparesEqualTo() {

  assertThat(1, comparesEqualTo(1));
 }

 /**
  * 対象オブジェクトのそれぞれに対して期待値と一致するかチェック。順番の整合性もチェック
  */
 @Test
 public void testメソッドチェック_contains() {

  assertThat(Arrays.asList("foo", "bar"), contains("foo", "bar"));
 }

 /**
  * 対象オブジェクトのそれぞれに対して期待値と一致するかチェック。順番の整合性はチェックしない
  */
 @Test
 public void testメソッドチェック_containsInAnyOrder() {

  assertThat(Arrays.asList("foo", "bar"), containsInAnyOrder("bar", "foo"));
 }

 /**
  * 対象文字列に期待文字列が含まれているかをチェック
  */
 @Test
 public void testメソッドチェック_containsString() {

  assertThat("myStringOfNote", containsString("ring"));
 }

 /**
  * エラー時に表示される文言を上書きする
  */
 @Test
 public void testメソッドチェック_describedAs() {

  String expected = "bbb";

  assertThat("aaa", describedAs(expected + ":文字列が一致していません。", is(expected)));
 }

 /**
  * 対象オブジェクトを二つのMacherでチェックして、どちらか一方でもtrueかどうかチェック
  */
 @Test
 public void testメソッドチェック_either() {

  assertThat("fan", either(containsString("a")).or(containsString("b")));
 }

 /**
  * 対象Colelctionオブジェクトが空かどうかをチェック
  */
 @Test
 public void testメソッドチェック_empty() {

  assertThat(new ArrayList(), is(empty()));
 }

 /**
  * 対象配列が空オブジェクトかどうかをチェック
  */
 @Test
 public void testメソッドチェック_emptyArray() {

  assertThat(new String[0], emptyArray());
 }

 /**
  * 対象配列オブジェクトが空かどうかをチェック。型のチェックも行う。
  */
 @Test
 public void testメソッドチェック_emptyCollectionOf() {

  assertThat(new ArrayList(), is(emptyCollectionOf(String.class)));
 }

 /**
  * 対象Iterableオブジェクトが空かどうかをチェック
  */
 @Test
 public void testメソッドチェック_emptyIterable() {

  assertThat(new ArrayList(), is(emptyIterable()));
 }

 /**
  * 対象Iterableオブジェクトが空かどうかをチェック。型のチェックも行う。
  */
 @Test
 public void testメソッドチェック_emptyIterableOf() {

  assertThat(new ArrayList(), is(emptyIterableOf(String.class)));
 }

 /**
  * 対象文字列の末尾が期待値文字列であるかどうかをチェック
  */
 @Test
 public void testメソッドチェック_endsWith() {

  assertThat("myStringOfNote", endsWith("Note"));
 }

 /**
  * 対象オブジェクトと期待値オブジェクトの一致チェック。オブジェクトのequalToメソッドで比較する。
  */
 @Test
 public void testメソッドチェック_equalTo() {

  assertThat("foo", equalTo("foo"));
  assertThat(new String[] {"foo", "bar"}, equalTo(new String[] {"foo", "bar"}));

 }

 /**
  * 対象文字列と期待値文字列の一致チェック。大文字と小文字を区別しない。
  */
 @Test
 public void testメソッドチェック_equalToIgnoringCase() {

  assertThat("Foo", equalToIgnoringCase("FOO"));

 }

 /**
  * 対象文字列と期待値文字列の一致チェック。空白スペースがある場合に空白スペースの数を意識しない。
  */
 @Test
 public void testメソッドチェック_equalToIgnoringWhiteSpace() {

  assertThat("   my\tfoo  bar ", equalToIgnoringWhiteSpace("   my  foo bar"));

 }

 /**
  * 対象イベントオブジェクトの発生元が期待値であるかをチェック
  */
 @Test
 public void testメソッドチェック_eventFrom() {

  EventObject eo = new PropertyChangeEvent(this, null, 0, "abcs");

  assertThat(eo, is(eventFrom(PropertyChangeEvent.class, this)));

 }

 /**
  * 対象オブジェクトの全ての要素に対して期待値を満たすかどうかをチェック
  */
 @Test
 public void testメソッドチェック_everyItem() {

  assertThat(Arrays.asList("bar", "baz"), everyItem(startsWith("ba")));

 }

 /**
  * 対象数値は期待数値より大きい(actual > expected)かどうかをチェック
  */
 @Test
 public void testメソッドチェック_greaterThan() {

  assertThat(2, greaterThan(1));

 }

 /**
  * 対象数値は期待数値より以上(actual >= expected)かどうかをチェック
  */
 @Test
 public void testメソッドチェック_greaterThanOrEqualTo() {

  assertThat(1, greaterThanOrEqualTo(1));

 }

 /**
  * 対象Mapに期待のkey,valueが入っているかチェック
  */
 @Test
 public void testメソッドチェック_hasEntry() {

  Map map = new HashMap();
  map.put("bar", "foo");

  assertThat(map, hasEntry(equalTo("bar"), equalTo("foo")));

 }

 /**
  * 対象Iterableオブジェクトの中に期待値を満たすオブジェクトが含まれているかどうかをチェック
  */
 @Test
 public void testメソッドチェック_hasItem() {

  assertThat(Arrays.asList("foo", "bar"), hasItem(startsWith("ba")));

 }

 /**
  * 対象配列オブジェクトの中に期待値を満たすオブジェクトが含まれているかどうかをチェック
  */
 @Test
 public void testメソッドチェック_hasItemInArray() {

  assertThat(new String[] {"foo", "bar"}, hasItemInArray(startsWith("ba")));

 }

 /**
  * 対象Iterableオブジェクトの中に期待値を満たすオブジェクトが含まれているかどうかをチェック
  * 期待値には複数条件を設定出来る。
  *
  */
 @SuppressWarnings("unchecked")
 @Test
 public void testメソッドチェック_hasItems() {

  assertThat(Arrays.asList("foo", "bar", "baz"), hasItems(endsWith("z"), endsWith("o")));

 }

 /**
  * 対象Mapに期待されるkeyが入っているかをチェック
  */
 @Test
 public void testメソッドチェック_hasKey() {

  Map map = new HashMap();
  map.put("bar", "foo");

  assertThat(map, hasKey(equalTo("bar")));

 }

 /**
  * 対象JavaBeansオブジェクトに期待する名称のプロパティが存在するかチェック
  */
 @Test
 public void testメソッドチェック_hasProperty() {

  Gene gene = new Gene();

  assertThat(gene, hasProperty("mem"));

 }

 /**
  * 対象Collectionオブジェクトのサイズチェック
  */
 @Test
 public void testメソッドチェック_hasSize() {

  assertThat(Arrays.asList("foo", "bar"), hasSize(2));

 }

 /**
  * 対象オブジェクトをtoStringした結果が期待文字列と一致するかチェック
  */
 @Test
 public void testメソッドチェック_hasToString() {

  assertThat(true, hasToString(equalTo("true")));

 }

 /**
  * 対象Mapに期待されるvalueが入っているかチェック
  */
 @Test
 public void testメソッドチェック_hasValue() {

  Map map = new HashMap();
  map.put("bar", "foo");

  assertThat(map, hasValue(equalTo("foo")));

 }

 /**
  * 対象XMLドキュメントに期待するパスが存在するかをチェック
  *
  * @throws ParserConfigurationException
  */
 @Test
 public void testメソッドチェック_hasXPath() throws ParserConfigurationException {

  DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
  DocumentBuilder docbuilder = dbfactory.newDocumentBuilder();

  Document document = docbuilder.newDocument();

  Element rootElement = document.createElement("root");
  document.appendChild(rootElement);

  Element element = document.createElement("chilr");
  rootElement.appendChild(element);

  assertThat(document, hasXPath("/root/chilr"));

 }

 /**
  * 対象オブジェクトが期待するクラスのインスタンスであるかをチェック
  */
 @Test
 public void testメソッドチェック_instanceOf() {

  assertThat(new Gene(), instanceOf(Gene.class));

 }

 /**
  * 対象オブジェクトが期待値と一致するかチェック
  */
 @Test
 public void testメソッドチェック_is() {

  assertThat("genesis", is(equalTo("genesis")));

 }

 /**
  * 対象オブジェクトのクラスが期待値のものであるかをチェック
  */
 @Test
 public void testメソッドチェック_isA() {

  assertThat(new Gene(), instanceOf(Gene.class));

 }

 /**
  * 対象文字列が空白、もしくはnullかどうかチェック
  */
 @Test
 public void testメソッドチェック_isEmptyOrNullString() {

  assertThat(((String)null), isEmptyOrNullString());

 }

 /**
  * 対象文字列が空白かどうかチェック。nullはNG
  */
 @Test
 public void testメソッドチェック_isEmptyString() {

  assertThat("", isEmptyString());

 }

 /**
  * 対象オブジェクトが期待するCollectionオブジェクトの中に含まれているかどうかをチェック
  * 期待値にCollectionをセットする。
  */
 @Test
 public void testメソッドチェック_isIn() {

  assertThat("foo", isIn(Arrays.asList("bar", "foo")));

 }

 /**
  * 対象オブジェクトが期待するCollectionオブジェクトの中に含まれているかどうかをチェック
  * 期待値に可変長引数をセットする。
  */
 @Test
 public void testメソッドチェック_isOneOf() {

  assertThat("foo", isOneOf("bar", "foo"));

 }

 /**
  * 対象Iterablesの型と要素数が期待値と一致するかをチェック
  */
 @Test
 public void testメソッドチェック_iterableWithSize() {

  assertThat(Arrays.asList("foo", "bar"), IsIterableWithSize.iterableWithSize(2));

 }

 /**
  * 対象数値は期待数値未満(actual < expected)かどうかをチェック
  */
 @Test
 public void testメソッドチェック_lessThan() {

  assertThat(1, lessThan(2));

 }

 /**
  * 対象数値は期待数値以下(actual <= expected)かどうかをチェック
  */
 @Test
 public void testメソッドチェック_lessThanOrEqualTo() {

  assertThat(1, lessThanOrEqualTo(1));

 }

 /**
  * macherの非定型を作成する。
  */
 @Test
 public void testメソッドチェック_not() {

  assertThat("aaa", is(not(equalTo("bbb"))));

 }

 /**
  * 対象オブジェクトがnullでないことをチェック
  */
 @Test
 public void testメソッドチェック_notNullValue() {

  assertThat("", is(notNullValue()));

 }

 /**
  * 対象オブジェクトがnullであることをチェック
  */
 @Test
 public void testメソッドチェック_nullValue() {

  assertThat(null, is(nullValue()));

 }

 /**
  * 対象オブジェクトが期待するオブジェクトと同じインスタンスであることをチェック
  */
 @Test
 public void testメソッドチェック_sameInstance() {

  Gene gene = new Gene();

  assertThat(gene, is(sameInstance(gene)));

 }

 /**
  * 対象文字列の先頭が期待値で始まっているかをチェック
  */
 @Test
 public void testメソッドチェック_startsWith() {

  assertThat("myStringOfNote", startsWith("my"));

 }

 /**
  * 対象文字列に対し、期待値の文字が順番で含まれているかをチェック
  */
 @Test
 public void testメソッドチェック_stringContainsInOrder() {

  assertThat("myfoobarbaz", stringContainsInOrder(Arrays.asList("foo","baz")));

 }

 /**
  * 対象オブジェクトが期待するクラスのインスタンスであるかをチェック
  */
 @Test
 public void testメソッドチェック_theInstance() {

  Gene gene = new Gene();

  assertThat(gene, is(theInstance(gene)));

 }

 /**
  * 対象オブジェクトが期待するクラスを継承しているかチェック
  */
 @Test
 public void testメソッドチェック_typeCompatibleWith() {

  assertThat(Integer.class, typeCompatibleWith(Number.class));

 }

 /**
  * テスト用クラス
  *
  */
 public class Gene{

  private String mem;

  public String getMem() {
      return mem;
  }

  public void setMem(String mem) {
      this.mem = mem;
  }

 }

}

2013年5月20日月曜日

JUnit4のススメ

初めまして。
株式会社ジェニシス 技術開発事業部の遠藤 太志郎です。

この度、我々ジェニシス技開では技術系の記事をメインにしたブログを開設し、
我々の持つ技術情報を全世界に発信していく試みを始めることになりました。

第一弾は私、遠藤が「テスト自動化シリーズ」と題しまして、しばらく連載させて頂こうと思います。
微力ではありますが、これを読まれた読者の方のお力になれれば幸いです。

では、本題に入ります。
「テスト自動化シリーズ」の最初は「JUnit4」です!!

……無論、世界一有名なテスト自動化ツールですので、皆さんご存じかと思います。
なので、今更JUnitなど語られても何の価値も無いと思う方もいらっしゃるでしょう。

しかし、今まで私が参加したプロジェクトを思い返してみると、
「何となくJUnitを使っている」だけで、JUnitに備わっている本来の機能を有効活用出来ていない人が非常に多かったです。
たまたま私の職場がそうだったというだけなら良いのですが、もしかしたらそこら中にそんな状態のプロジェクトが散在しているのかも、と思い今回、ブログ記事にしようと思いました。

では、私の言う「何となく使っている人」の例を挙げましょう。


まず、テスト対象のソースとして以下を定義します。

/**
 * 文字列を結合して返却します。
 *
 * @param str1
 * @param str2
 * @return
 */
public String getConnectString(String str1,String str2) {
 return str1 + str2;
}

これに対し、JUnitのソースはこちら!!

@Test
public void testGetConnectString() {

 Sample001 sample001 = new Sample001();

 String str1 = "株式会社ジェニシス";
 String str2 = "技術開発事業部";

 assertEquals("株式会社ジェニシス技術開発事業部", sample001.getConnectString(str1, str2));
}

こんなJUnitソースを書いている人がいたら、それは「何となく使っている人」の疑惑が非常に高いです。

何がダメかと言うと、「assertEquals」の部分です。
これはJUnit3の書き方でして、JUnit4に移行しても過去のソースが動くように互換性を持たせているだけで、本来JUnit4では非推奨としている書き方です。
JUnit4の書き方は以下です。

@Test
public void testGetConnectString4() {

 Sample001 sample001 = new Sample001();

 String str1 = "株式会社ジェニシス";
 String str2 = "技術開発事業部";

 assertThat(sample001.getConnectString(str1, str2), is("株式会社ジェニシス技術開発事業部"));
}

「assertThat」が登場しています。
ここがJUnit4のスタート地点です。

JUnit4を使うことで、エラー時に発生する文言が詳細になったり、Matcherインターフェースのような機能が使えるようになるなど、様々なメリットを得られます。


ソースの読み易さとしても、
「assert that actual_param is expected_param」と英語として成立しているのです。
英語圏の人ならより一層読みやすいわけです。


上述の高機能については次回以降の記事で記載しますが、
エラー時の文言の違いは以下に記載しておきます。

assertEqualsの文言

assertThatの文言


やはり「assertThat」の方が読みやすい文言かと思います。

今までJUnit3な書き方をしていた人は、これを機にぜひJUnit4に乗り換えて下さい。

次回は「Matcherインターフェース」をご紹介します。

余談

プロジェクトの現場で未だJUnit3な書き方が多いのは、一重に「Eclipseの標準仕様」が原因であると私は考えています。

Eclipseには『新規作成⇒JUnitテスト・ケース』というJUnitクラスの自動作成機能があります。
これを使うとテスト対象メソッドに対応したJUnitメソッドを自動出力してくれるので便利なのですが、この時、「import static org.junit.Assert.*;」が自動インポートされます。
これが自動作成直後の初期状態です。

しかし、この状態では「assertThat」の中で使っている「is」メソッドが使えません。
「is」メソッドを使うには、別途手動で、
「import static org.hamcrest.CoreMatchers.is;」
をインポートする手間が必要です。

しかし、「assertEquals」ならば、何もしなくても即使えます。

つまり、Eclipseの自動機能に頼ると自然とJUnit3の書き方に誘導されていくわけです。
これがJUnit4普及の障害になっているのではないでしょうか。

これをお読みになった皆様にはぜひ、この初期インポートの一手間を乗り越えて、JUnit4を活用して頂きたいです。