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を活用して頂きたいです。