joda-timeとhamcrest

2012-12-05T00:00:00+00:00 Java

以前からやろうと思ってた物シリーズ的な感じ。日付処理系でjoda-time、んで単体テストのマッチャにhamcrestを使ってみた

package sample.test;

import java.util.Date;

import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.Duration;
import org.junit.Test;

import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;

public class JodaTestCase1 {

    @Test
    public void test1() {
        DateTime dt1 = DateTime.now();

        assertThat(dt1, is(notNullValue()));
        assertThat(dt1.getYear(), is(2012));
        assertThat(dt1.getMonthOfYear(), is(12));
        assertThat(dt1.getDayOfMonth(), is(4));
        assertThat(dt1.plusDays(1).getDayOfMonth(), is(5));

        assertThat(dt1.toDate(), instanceOf(Date.class));
        assertThat(dt1.toDate().getTime(), is(dt1.getMillis()));

        DateTime dt2 = dt1.dayOfMonth().withMaximumValue();

        assertThat(dt2, is(notNullValue()));
        assertThat(dt2.getYear(), is(2012));
        assertThat(dt2.getMonthOfYear(), is(12));
        assertThat(dt2.getDayOfMonth(), is(31));
        assertThat(dt2.isAfter(dt1), is(true));

        DateTime dt3 = dt2.plusDays(1);

        assertThat(dt3, is(notNullValue()));
        assertThat(dt3.getYear(), is(2013));
        assertThat(dt3.getMonthOfYear(), is(1));
    }

    /* Durationの使い方目的な為パス
    @Test
    public void test2() {
        DateTime dt1 = new DateTime(2012, 2, 1, 0, 0, 0);
        DateTime dt2 = dt1.dayOfMonth().withMaximumValue();
        Duration dur = new Duration(dt1, dt2);
        Days days = dur.toStandardDays();

        assertThat(days.getDays(), is(28));
    }
    */
}

な感じ。でDateTimeを主に使うのですが、これDateTimeはimmutableな模様。なのでMutableなDateTimeを使う場合にはMutableDateTimeを使う模様

あとは大体はメソッド名とかで何やってるかは分かると思うのでパス。ただ検証が昨日やってたのでアサーションベースが昨日をベースになってる

でisだとかのAPI自体がhamcrestなAPIになってる。assertEqualsとかでチェックするよりはhamcrestベースでassertThatを使う方が良い感じだとの事。まぁ他にもフォーマッタとかコンバーターとか色々ある模様で

でちなみにGoogle App Engine/Javaではcom.google.appengine.repackaged.org.joda.timeっていうパッケージでバンドルされる模様なので普通に使える模様

余談: hamcrestのマッチャを作る

例えば

package sample.test;

import org.joda.time.DateTime;
import org.junit.Test;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static sample.test.matcher.DateTimeMatcher.isDateTime;

public class JodaTestCase2 {
    @Test
    public void test1() {
        DateTime dt1 = DateTime.now();

        assertThat(dt1, is(notNullValue()));
        assertThat(dt1, isDateTime(new DateTime(2012, 12, 4, 0, 0,0)));
    }
}

みたいにisDateTimeで年月日をチェックするのを作ってみる。あんまり意味ない感じだけど、あくまで「マッチャの作り方」なんで。まぁ上記に書いてるように「sample.test.matcher.DateTimeMatcher」を作る

package sample.test.matcher;

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

import org.joda.time.DateTime;
import org.joda.time.DateTimeFieldType;

public class DateTimeMatcher extends BaseMatcher<DateTime> {

    private final DateTime dateTime;

    private DateTimeFieldType errorDescriptionProperty;

    public DateTimeMatcher(DateTime dateTime) {
        this.dateTime = dateTime;
    }

    @Override
    public boolean matches(Object o) {
        if (o instanceof DateTime) {
            DateTime expected = DateTime.class.cast(o);

            int expectedYear = expected.getYear();
            int expectedMonth = expected.getMonthOfYear();
            int expectedDay = expected.getDayOfMonth();

            if (dateTime.getYear() == expectedYear) {
                if (dateTime.getMonthOfYear() == expectedMonth) {
                    if (dateTime.getDayOfMonth() == expectedDay) {
                        return true;
                    } else {
                        errorDescriptionProperty = DateTimeFieldType.dayOfMonth();
                    }
                } else {
                    errorDescriptionProperty = DateTimeFieldType.monthOfYear();
                }
            } else {
                errorDescriptionProperty = DateTimeFieldType.year();
            }
        }

        return false;
    }

    @Override
    public void describeTo(Description description) {
        if (dateTime != null && errorDescriptionProperty != null) {
            description.appendText(
                errorDescriptionProperty.getName()
            ).appendValue(
                dateTime.get(errorDescriptionProperty)
            );
        }
    }

    @Override
    public void describeMismatch(Object item, Description description) {
        if (item instanceof DateTime) {
            DateTime errorExpectedDateTime = DateTime.class.cast(item);

            if (errorDescriptionProperty != null) {
                description.appendText(
                    errorDescriptionProperty.getName()
                ).appendValue(
                    errorExpectedDateTime.get(errorDescriptionProperty)
                );

                return;
            }
        }

        super.describeMismatch(item, description);
    }

    @Factory
    public static Matcher<DateTime> isDateTime(DateTime dateTime) {
        return new DateTimeMatcher(dateTime);
    }
}

ざっくり書くとこんな感じかと。BaseMatcherってのを継承して作るんだそうで。でdescribeMismatchは別にデフォルトでオーバーライドしなくても良いけど、ちゃんとテストがfailした場合の理由も出したいのでね

でDateTimeから特定のフィールドなのを出したい場合にはDateTime.getメソッドでDateTimeFieldTypeを指定すれば良い模様

FuelPHPをやってみる (24) - Goutteでコントローラーテスト - Chrome Extension開発を勉強してみる (21) - chrome.experimental.commands - の補足