2011年12月24日土曜日

hamcrestを拡張してmoreThanとか作ってみた

JavaAdventCalendarの24日目のエントリーです。

最初はenumに関してエントリーを書こうとしていたのですが、そのネタについてコードを書いている途中で、どうしても気持ち悪いテストコードを書く羽目になったので、なんかいい表現ないかなと考えているうちに、hamcrestの拡張を書いてしまいました。

というわけで、タイトル

hamcrestを拡張してmoreThanとか作ってみた


です。

そもそものそもそも


もともと書いていた気持ち悪いテストコード…

@Test
    public void testCompare() {
        Trump queen = Trump.valueOf("Queen");
        Trump king = Trump.valueOf("King");
        assertThat(queen.compareTo(king) < 0, is(true));
    }

    private enum Trump implements Comparable<Trump> {
        King {
            int value = 13;
            @Override
            public int value() {
                return value;
            }
            @Override
            public int compareTo(Trump trump) {
                return this.value() - trump.value();
            }
        }, Queen {
            int value = 12;
            @Override
            public int value() {
                return value;
            }
            @Override
            public int compareTo(Trump trump) {
                return this.value() - trump.value();
            }
        }
        abstract public int value();
        abstract public int compareTo(Trump trump);
    }


assertThatの中が気持ち悪い…

まあ、別にboolean result = queen.compareTo(king) < 0を変数として取り出せばよいだけの話といえば、それまでなのですが、やっぱりなんか気持ち悪い。

で、junit more thanググってみたけど、こんな感じでした(´・ω・`)

というわけで、moreThanみたいなことをやりたかったので、自前で作って見ることにしました。

拡張してみよう!


で、コードはこんな感じ。

org.hamcrest.core.MoreThan.java

package org.hamcrest.core;

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

public class MoreThan<T extends Comparable<T>> extends BaseMatcher<T> {

    private final T matcher;
    private final Class<T> klass;

    public MoreThan(T matcher) {
        this.matcher = matcher;
        this.klass = (Class<T>) matcher.getClass();
    }

    @Override
    public boolean matches(Object o) {
        if(o.getClass() == klass) {
            T object = klass.cast(o);
            int result = matcher.compareTo(object);
            return result < 0;
        } else {
            return false;
        }
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("more than ").appendValue(matcher);
    }

    @Factory
    public static <T extends Comparable<T>> Matcher<T> moreThan(T value) {
        return new MoreThan<T>(value);
    }
}


拡張して自分好みのMatcherを作るにはorg.hamcrest.BaseMatcher<T>を継承するようです。

メソッドmatchesは実際の値の比較を記述します。
メソッドdescribeToはテストが失敗したときに表示される文章を記述します。

使い方


使い方は至って簡単です。

MoreThanTest.java

@Test
    public void testCalendarCase() {
        Calendar cal1 = Calendar.getInstance(
            TimeZone.getTimeZone("Asia/Tokyo"));
        cal1.set(2011, 12, 24, 15, 54, 55);
        Calendar cal2 = Calendar.getInstance(
            TimeZone.getTimeZone("Asia/Tokyo"));
        cal2.set(2011, 12, 24, 15, 54, 56);
        assertThat(cal2, moreThan(cal1));
    }


ちなみに、残念な所があります。
  • intlongなど、異なる型の値を比較できない。

まあ、そのあたりは型安全様にあわせてあげて下さい。

その他、LessThanLessThanEqualMoreThanEqualなども作ってみましたので、まあ、興味があったら見てツッコミを下さい。

明日のJava Adventカレンダーは、daisuke-mさんです。


余談


このエントリーを掲載した後、こんなツイートが寄せられました。




うぉ、本当だ…!


さらには、



なんと!。
JUnit4にバンドルされているhamcrestは古いバージョンだと!

そして、極めつけは、


というわけで、明日のdaisuke_mさんのページを見ると…

都元ダイスケ IT-PRESS : [Java][test]hamcrestのMatcherメモ

バッチリまとめられておる。


というわけで、車輪の再発明をしてしまったようだ…。


そこで、ソースを読んでみる


まあ、情弱なのは仕方ないので、もうちょい突っ込んでみてみる。


hamcrest-libraryのソースはcode.google.comにあるようです。

org.hamcrest.Matchers.javaというソースはなかったのですが、その本体となるorg.hamcrest.number.OrderingComparisonというクラスがあったので、それを読んでみました。


/*  Copyright (c) 2000-2009 hamcrest.org
 */
package org.hamcrest.number;

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

public class OrderingComparison> extends TypeSafeMatcher {
    private static final int LESS_THAN = -1;
    private static final int GREATER_THAN = 1;
    private static final int EQUAL = 0;
    private final T expected;
    private final int minCompare, maxCompare;

    private static final String[] comparisonDescriptions = {
            "less than",
            "equal to",
            "greater than"
    };

    private OrderingComparison(T expected, int minCompare, int maxCompare) {
        this.expected = expected;
        this.minCompare = minCompare;
        this.maxCompare = maxCompare;
    }

    @Override
    public boolean matchesSafely(T actual) {
        int compare = Integer.signum(actual.compareTo(expected));
        return minCompare <= compare && compare <= maxCompare;
    }

    @Override
    public void describeMismatchSafely(T actual, Description mismatchDescription) {
        mismatchDescription.appendValue(actual).appendText(" was ")
                .appendText(asText(actual.compareTo(expected)))
                .appendText(" ").appendValue(expected);
    }

    public void describeTo(Description description) {
        description.appendText("a value ").appendText(asText(minCompare));
        if (minCompare != maxCompare) {
            description.appendText(" or ").appendText(asText(maxCompare));
        }
        description.appendText(" ").appendValue(expected);
    }

    private String asText(int comparison) {
        return comparisonDescriptions[comparison + 1];
    }

    /**
     * @return Is value = expected?
     */
    @Factory
    public static > Matcher comparesEqualTo(T value) {
        return new OrderingComparison(value, EQUAL, EQUAL);
    }

    /**
     * Is value > expected?
     */
    @Factory
    public static > Matcher greaterThan(T value) {
        return new OrderingComparison(value, GREATER_THAN, GREATER_THAN);
    }

    /**
     * Is value >= expected?
     */
    @Factory
    public static > Matcher greaterThanOrEqualTo(T value) {
        return new OrderingComparison(value, EQUAL, GREATER_THAN);
    }

    /**
     * Is value < expected?
     */
    @Factory
    public static > Matcher lessThan(T value) {
        return new OrderingComparison(value, LESS_THAN, LESS_THAN);
    }

    /**
     * Is value <= expected?
     */
    @Factory
    public static > Matcher lessThanOrEqualTo(T value) {
        return new OrderingComparison(value, LESS_THAN, EQUAL);
    }
}



@Factoryのあたりを眺めてから、describeToなどを眺めると、思わずニヤリとしてしまいますね。
やっぱり本家本元のソースには勝てんかったか…。


で、なんで、Matchersがないのか気になったのですが、ひょっとしてhamcrest-generatorあたりで自動で処理しているのかな~などと思ってみたりしましたが、まだそこまでちゃんとソースを読んでおりません…

ん~、まあ、でも少し勉強になりました。

ご指摘をくださった皆様、大変有難う御座います。




0 件のコメント:

コメントを投稿