hamcrest の Matcher を独自拡張 - java.util.Date 版 closeTo

テスト対象クラスの内部で new Date() しているようなコードをテストする場合、テストクラス側で new Date() した値とはミリ秒程度の誤差が出てしまうことがあります。
hamcrest の Matcher で、double の誤差を許容するためのメソッド closeTo があります。これの java.util.Date 版があれば解決するんじゃね? ・・・と思ったのですが、どうやらデフォルトでは提供されていないようです。残念。

というわけで、自作しました。


比較クラスの実装

package enhance;

import java.util.Date;

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

/**
 * Dateの誤差吸収用 {@link Matcher} です。<p>
 * thanks to {@link org.hamcrest.number.IsCloseTo}
 */
public class IsCloseTo extends TypeSafeMatcher<Date> {
    private final long delta;
    private final Date base;
    
    public IsCloseTo(Date base, long errorMillis) {
        this.base = base;
        this.delta = errorMillis;
    }
    @Override
    public void describeTo(Description description) {
        description.appendText("a date value within ")
                .appendValue(delta)
                .appendText(" millisecond of ")
                .appendValue(base);
    }
    
    @Override
    protected void describeMismatchSafely(Date item, Description mismatchDescription) {
        mismatchDescription.appendValue(item)
                        .appendText(" differed by ")
                        .appendValue(actualDelta(item));
    }

    @Override
    protected boolean matchesSafely(Date item) {
        return actualDelta(item);
    }
    
    private boolean actualDelta(Date item) {
        long deltaMillis = base.getTime() - item.getTime();
        return Math.abs(deltaMillis) <= delta;
    }
    
    @Factory
    public static Matcher<Date> closeTo(Date operand, long errorMillis) {
        return new IsCloseTo(operand, errorMillis);
    }
}


static import 用のメソッド実装

package enhance;

/**
 * thanks to {@link org.hamcrest.Matchers}.
 */
public class Matchers {
    public static org.hamcrest.Matcher<java.util.Date> closeTo(java.util.Date operand, long errorMillis) {
        return enhance.IsCloseTo.closeTo(operand, errorMillis);
    }
}


@Factory アノテーションは、つけておくと hamcrest-genarator というツールを実行したときに Matchers クラスの static メソッドを自動生成してくれるようです(たぶん)。今回は別にあってもなくても構わないのですが、元ソースにならって記述しておきました。
実質、 matchesSafely メソッドをオーバーライドして緩い比較をしてやるだけですので、それほど大したことはやっていません。


以下は、(動作テストも兼ねた)実際の使用例です。

package enhance;

import static enhance.Matchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.Date;
import org.junit.Test;

public class CloseToTest {

    @Test
    public void date版closeToの使用例() {
        Date actual = Util.getDate();
        Date expected = new Date();
        
        assertThat(actual, is(not(expected))); // 通常のDate比較
        assertThat(actual, closeTo(expected, 1000)); // 1秒以内の誤差を許容する比較
    }
    
    static class Util {
        static Date getDate() {
            Date now = new Date();
            try {
                Thread.sleep(900); // わざと0.9秒停止
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return now;
        }
    }
}