Hamcrestチュートリアル

以下はThe Hamcrest Tutorialの翻訳(2018/6/26時点)

Hamcrestの使い方の概要となる。また、訳注としてJUnit 4で使用する場合の注意点も記述している。ちなみにHamcrestの日本語での読み方はどう考えても「ハムクレスト」だろう。Hamcrestは辞書には載っていない、それもそのはず、Matchersのアナグラム(文字の順番を変えたもの)だそうだ。

イントロ

Hamcrestはマッチャーオブジェクトを書くためのフレームワークである。これは、マッチルールを宣言的に書くことができる。マッチャーが有益な状況は多数あるだろう。UIの検証やデータフィルタリングである。しかし、マッチャーが最も使われるのは、フレキシブルなテスト作成の領域だ。このチュートリアルでは、いかにしてHamcrestをユニットテストに使うかを見ていく。

テストを書く場合、ときに難しくなるのはその正しいバランスだ。つまり、テストをオーバースペックにしてしまう(そして変更に対して脆弱になる)か、あるいは、十分に既定していないか(テストはそれほど重要ではなくなる、なぜならテスト済のものが壊れたところで、パスし続けるからだ)のあいだのバランスである。テスト下の様相を正確に取り出し、持つべき値を記述でき、正確さのレベルをコントロールできるようなツールが、「ちょうどよい」テストを書くための大いなる助けになる。このようなテストでは、期待されるふるまいから外れるような振る舞いでは失敗するのだが、それでも、些細で振る舞いに無関係な変更の場合はパスし続ける。

最初のHamcrestによるテスト

非常に単純なJUnit 3のテストから始めるが、しかし、JUnitのassertEqualsメソッドではなく、HamcrestのassertThatコンストラクトとマッチャーの標準的セットを使うことにする。両者ともstaticなimportができる。

import static org.hamcrest.MatcherAssert.assertThat; 
import static org.hamcrest.Matchers.*;

import junit.framework.TestCase;

public class BiscuitTest extends TestCase { 
  public void testEquals() { 
    Biscuit theBiscuit = new Biscuit("Ginger"); 
    Biscuit myBiscuit = new Biscuit("Ginger"); 
    assertThat(theBiscuit, equalTo(myBiscuit)); 
  } 
}

assertThatメソッドはテストアサーションのための定型化文だ。この例では、アッサーションの対象はbiscuitオブジェクトだ。これがメソッドの最初の引数になる。二つ目の引数は、Biscuitオブジェクトのマッチャーだ。この例では、一つのオブジェクトがもう一つに等しいかをチェックするためにObject#equals()を使うマッチャーだ。Buscuitクラスがequalsメソッドを定義しているため、このテストはパスする。

テスト内に複数のアサーションがある場合には、アサーション内のテスト知に識別子を含めることができる。

  assertThat("chocolate chips", theBiscuit.getChocolateChipCount(), equalTo(10)); 
  assertThat("hazelnuts", theBiscuit.getHazelnutCount(), equalTo(3)); 

訳注1:JUnitのassertEquals()では、第一引数に期待値、第二引数にテスト対象値とするが、Hamcrestの引数二つの場合のassertThatでは逆になっている。第一引数がテスト対象になり、第二引数はMatcherであり、検証内容によって第二引数が変化することになる。つまり、JUnitのような、assert云々というメソッドは存在せず、常にassertThatを使うことになる。

訳注2:例えばJUnit4.12はhamcrest-coreに依存しているのだが、これだけではHamcrestに用意されているマッチャーは使用できない。この依存を削除し、hamcrest-allを依存に加えた方がよい。Gradleの場合は以下になる。

dependencies {    
  ....

  testCompile(group: 'junit', name: 'junit', version: '4.12') {
    exclude module:'hamcrest-core'
  }
  testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'
}

その他のテストフレームワーク

Hamcrestは、様々なユニットテストフレームワークできるようデザインされている。例えば、HamcrestはJUnit 3、JUnit 4、TestNGと共に使用することができる(詳細については、Hamcrestのフルディストリニューション中の例を見てほしい)。既存のテストスタイルを、Hamcrestスタイルのアサーションに移行することは極めて容易だ、他のアサーションスタイルがHamcrestのものと共存できるからである。

Hamcrestはまた、モックオブジェクトフレームワークと共に使用することができる、アダプタを使うことにより、モックオブジェクトフレームワークでのマッチャーコンセプトからHamcrestのマッチャーに橋渡しできる。例えば、JMock 1の製薬はHamcrestのマッチャーである(相当する?)。HamcrestはJMock 1用のアダプタを提供し、それにより、JMock 1のテスト中にHamcrestマッチャーを使うことができる。JMock 2では、このようなアダプタレイヤは不要だ。Hamcrestライブラリをマッチングライブラリとして使うようデザインされているからだ。Hamcrestはまた、EasyMock 2用のアダプタも提供している。再度、より詳細はHamcrestのサンプル集を見てほしい。

一般的マッチャーのツアー

Hamcrestは役に立つマッチャーのライブラリが用意されている。以下はいくつかの重要なものだ。

Core

  • anything – 常にマッチする。どんなオブジェクトであるか気にしない場合に有用

  • describedAs – decorator to adding custom failure description

  • is – decorator to improve readability – see “Sugar”, below Logical

  • allOf – matches if all matchers match, short circuits (like Java &&)

  • anyOf – matches if any matchers match, short circuits (like Java ||)

  • not – matches if the wrapped matcher doesn’t match and vice versa Object

  • equalTo – test object equality using Object.equals

  • hasToString – test Object.toString

  • instanceOf, isCompatibleType – test type

  • notNullValue, nullValue – test for null

  • sameInstance – test object identity Beans

  • hasProperty – test JavaBeans properties Collections

  • array – test an array’s elements against an array of matchers

  • hasEntry, hasKey, hasValue – test a map contains an entry, key or value

  • hasItem, hasItems – test a collection contains elements

  • hasItemInArray – test an array contains an element Number

  • closeTo – test floating point values are close to a given value

  • greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo – test ordering Text

  • equalToIgnoringCase – test string equality ignoring case

  • equalToIgnoringWhiteSpace – test string equality ignoring differences in runs of whitespace

  • containsString, endsWith, startsWith – test string matching Sugar

Hamcrestは可能な限りテストを読みやすくしようとする。例えば、isは、その下のマッチャーに対して、何の特別な振る舞いをも追加しないラッパとしてのマッチャーだ。以下のアサーションは等価である。

assertThat(theBiscuit, equalTo(myBiscuit)); 
assertThat(theBiscuit, is(equalTo(myBiscuit))); 
assertThat(theBiscuit, is(myBiscuit));

三番目が許される理由は、is(T value)が、is(equalsTo(value))を返すようオーバーロードされているからだ。

訳注:いくつかのマッチャーを使用してみた例を以下に示す。このテストは成功する。

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

import java.util.*;

import org.junit.*;

public class Sample {
  @Test
  public void test() {
    Map<String, String>map = new HashMap<>();
    map.put("a", "b");    
    assertThat(map, instanceOf(HashMap.class));
    assertThat(map, hasEntry("a", "b"));
    assertThat(map, hasKey("a"));
    assertThat(map, not(hasKey("c")));
    assertThat(map, notNullValue());
  }
}

カスタムマッチャーの記述

Hamcrestには多くの役に立つマッチャーがバンドルされているが、おそらくときには、テストの必要性に応じて自身のマッチャーを作る必要性があるだろう。これが良く起こるのは、同じプロパティの集合を異なるテストにおいて何度もテストするような場合だ。これを単一のアサーションにまとめてしまいたいと思うだろう。自身のマッチャーを記述することにより、そういったコードの重複を除去することができ、より読みやすくなるのだ。

double値がNaN(数値ではない)かをチェックするテストを書いてみよう。書きたいのか以下だ。

public void testSquareRootOfMinusOneIsNotANumber() { 
  assertThat(Math.sqrt(-1), is(notANumber())); 
}

実装は以下になる。

package org.hamcrest.examples.tutorial;

import org.hamcrest.Description; 
import org.hamcrest.Factory; 
import org.hamcrest.Matcher; 
import org.hamcrest.TypeSafeMatcher;

public class IsNotANumber extends TypeSafeMatcher {

  @Override 
  public boolean matchesSafely(Double number) { 
    return number.isNaN(); 
  }

  public void describeTo(Description description) { 
    description.appendText("not a number"); 
  }

  @Factory 
  public static Matcher notANumber() { 
    return new IsNotANumber(); 
  }
} 

assertThatメソッドはジェネリックメソッドであり、アサーションの対象型でパラメとライズされたマッチャーを受け取る。現在Double値についてのアサーションを行っており、Matcherが必要であることがわかっている。我々のマッチャーの実装では、TypeSafeMatcherのサブクラスとして作成するのが便利だ。ここでは、自動でDoubleへのキャストを行ってくれる。実装が必要なのはmatchesSafelyメソッドで、単純にDouble値がNaNであるかを調べるだけだ。それとdescribeToメソッドで、ここでは、テスト失敗時のメッセージを作成する。テスト失敗メッセージがどのようなものか例を示す。

assertThat(1.0, is(notANumber()));

// fails with the message

java.lang.AssertionError: Expected: is not a number got : <1.0>

我々のマッチャーの三番目のメソッドは、便利なファクトリメソッドである。このメソッドをstaticにインポートして、テストのためのマッチャーを使う。

import static org.hamcrest.MatcherAssert.assertThat; 
import static org.hamcrest.Matchers.*;

import static org.hamcrest.examples.tutorial.IsNotANumber.notANumber;

import junit.framework.TestCase;

public class NumberTest extends TestCase {

  public void testSquareRootOfMinusOneIsNotANumber() { 
    assertThat(Math.sqrt(-1), is(notANumber())); 
  } 
} 

notANumber()メソッドが呼び出されるたびに、新たなマッチャーを作り出すのだが、これが唯一の使用パターンとは思わないでほしい。したがって、新たに作成するマッチャーはステートレスである必要がある。つまり、単一のインスタンスが、再利用されるということである。

Sugar generation (TBD)

いくつかのカスタムマッチャーを作るうちに、それぞれをimportするのが面倒に感じてくるだろう。一つのグループにまとめてしまうのが良いだろう、そうすれば、単一のstatic importで済むからだ。Hamcrestライブラリのマッチャーと同じように。Hamcrestにはこれを助ける仕組みがある、Generatorを使うのだ。

最初に、すべてのマッチャークラスをリストしたXMLのコンフィギュレーションファイルを作成する。この中のorg.hamcrest.Factoryアノテーションのついたファクトリメソッドが検索されることになる。例えば、

TBD

次に、Hamcrestに付属のorg.hamcrest.generator.config.XmlConfiguratorコマンドラインツールを起動しよう。このツールは、XMLコンフィギュレーションファイルを受け入れ、単一のJavaクラスを生成する。そこには、XMLファイルで指定されたすべてのファクトリメソッドが含まれる。引数無しで起動すると使用方法を表示することになる。こちらが出力例だ。

package org.hamcrest.examples.tutorial;

public class Matchers {

  public static org.hamcrest.Matcher is(T param1) { 
    return org.hamcrest.core.Is.is(param1); 
  }

  public static org.hamcrest.Matcher is(java.lang.Class param1) { 
    return org.hamcrest.core.Is.is(param1); 
  }

  public static org.hamcrest.Matcher is(org.hamcrest.Matcher param1) { 
    return org.hamcrest.core.Is.is(param1); 
  }

  public static org.hamcrest.Matcher notANumber() { 
    return org.hamcrest.examples.tutorial.IsNotANumber.notANumber(); 
  }

} 

最後に、新たなマッチャークラスを使うようにテストを更新しよう。

import static org.hamcrest.MatcherAssert.assertThat;

import static org.hamcrest.examples.tutorial.Matchers.*;

import junit.framework.TestCase;

public class CustomSugarNumberTest extends TestCase {

  public void testSquareRootOfMinusOneIsNotANumber() { 
    assertThat(Math.sqrt(-1), is(notANumber())); 
  } 
} 

注意すべきは、今や我々が使っているHamcrestライブラリのマッチャーは、我々自身のカスタムマッチャークラスからインポートされたものということである。