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を駆使して、ぜひ綺麗で強固なシステムを作り上げて下さい。

0 件のコメント:

コメントを投稿