ひよっこ。

I want to…

hamcrestで年月日比較がしたくなったのでDateMatcherを作ってみた

Posted by hikaruworld : 2010 10月 21

とりあえず、車輪の再発明な気がします。
どっかにきっとあると思うんだ。

以下、本題です。

hamcrestパッケージの叙述的?記法は結構気に入ってるのですが、
Date系のMatcherがあんまりなくて、今のテストだと、
年だけとか、月だけとか、日だけの検証がしたくなってきました。

hamcrestのMatcherクラスは結構簡単に拡張出来るので書いてみました。
以下のメソッドを利用して比較出来ます

// 年・月・日が正しいことを比較
assertThat(Date, isDate(Date));
// 年が正しいことを比較
assertThat(Date, isYear(int));
// 月が正しいことを比較
assertThat(Date, isMonth(int));
// 日が正しいことを比較
assertThat(Date, isDay(int));

以下ソースです。

import java.util.Calendar;
import java.util.Date;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
/**
 * Date比較用のMatcherクラス.
 * 年、月、日、あるいは年月日を対象に値の検証を行う
 * @author morikawa
 */
public class DateMatcher extends BaseMatcher<Date> {

    /** Matcher対象のオブジェクト */
    private Date entity;
    /** 出力メッセージ */
    private StringBuilder message;
    /** 検証対象フィールド */
    private DateField field;
    /** 比較フィールドの列挙型 **/    
    public static enum DateField {
        /** インスタンス自体の比較 */
        ALL {
            @Override
            public boolean is(Date base, Date target) {
                return YEAR.is(base, target) && MONTH.is(base, target) && DAY.is(base, target);
            }

            @Override
            public void addDescription(Description description, Date target) {
                description.appendValue(target);
            }
        },
        /** 年比較 */
        YEAR {
            @Override
            public boolean is(Date base, Date target) {
                Calendar baseCal = Calendar.getInstance();
                Calendar targetCal = Calendar.getInstance();
                baseCal.setTime(base);
                targetCal.setTime(target);

                return baseCal.get(Calendar.YEAR) == targetCal.get(Calendar.YEAR);
            }

            @Override
            public void addDescription(Description description, Date target) {
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(target);
                description.appendValue("YEAR::" + calendar.get(Calendar.YEAR));
            }
        },
        /** 月比較 */
        MONTH {
            @Override
            public boolean is(Date base, Date target) {
                Calendar baseCal = Calendar.getInstance();
                Calendar targetCal = Calendar.getInstance();
                baseCal.setTime(base);
                targetCal.setTime(target);

                return baseCal.get(Calendar.MONTH) == targetCal.get(Calendar.MONTH);
            }

            @Override
            public void addDescription(Description description, Date target) {
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(target);
                description.appendValue("MONTH::" + calendar.get(Calendar.MONTH));
            }
        },
        /** 日比較 */
        DAY {
            @Override
            public boolean is(Date base, Date target) {
                Calendar baseCal = Calendar.getInstance();
                Calendar targetCal = Calendar.getInstance();
                baseCal.setTime(base);
                targetCal.setTime(target);

                return baseCal.get(Calendar.DAY_OF_MONTH) == targetCal.get(Calendar.DAY_OF_MONTH);
            }

            @Override
            public void addDescription(Description description, Date target) {
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(target);
                description.appendValue("DAY_OF_MONTH::" + calendar.get(Calendar.DAY_OF_MONTH));
            }
        };
        /**
         * 値が同じであることの比較を行う。
         * @param base 比較元のDate
         * @param target 比較対象のDate
         * @return 値が同一の場合にtrue
         */
        abstract boolean is(Date base, Date target);
        /**
         * Matcherに検証対象オブジェクトの情報を設定する
         * @param description 概要
         * @param date 対象のDate
         */
        abstract void addDescription(Description description, Date target);
    }
    
    public DateMatcher(Date date, DateField field) {
        this.entity =  date;
        if (field == null) {
            this.field = DateField.ALL;
        } else {
            this.field = field;
        }
        this.message = new StringBuilder();
    }
    /**
     * 年月日レベルで比較を行う
     * @param date 比較対象のDate
     * @return 年月日比較で用いられる{@link DateMatcher}
     */
    public static DateMatcher isDate(Date date) {
        return new DateMatcher(date, DateField.ALL);
    }
    /**
     * 年比較を行う
     * @param year YYYY形式の日付
     * @return 年比較で用いられる{@link DateMatcher}
     */
    public static DateMatcher isYear(int year) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, year);
        return new DateMatcher(calendar.getTime(), DateField.YEAR);
    }
    /**
     * 月比較を行う
     * @param month 1~12までの月
     * @return 月比較を行う{@link DateMatcher}
     */
    public static DateMatcher isMonth(int month) {
        if (month > 12 || month < 1) {
            throw new IllegalArgumentException("対象外の月が設定されています");
        }
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.MONDAY, month -1);
        return new DateMatcher(calendar.getTime(), DateField.MONTH);
    }
    /**
     * 日比較を行う
     * @param day 1~31までの日
     * @return 日比較を行う{@link DateMatcher}
     */
    public static DateMatcher isDay(int day) {
        if (day > 31 || day < 1) {
            throw new IllegalArgumentException("対象外の日が設定されています");
        }
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.DAY_OF_MONTH, day);
        return new DateMatcher(calendar.getTime(), DateField.DAY);
    }

    /**
     * {@inheritDoc}
     * @see org.hamcrest.Matcher#matches(java.lang.Object)
     */
    public boolean matches(Object item) {
        Date date = (Date) item;
        if (this.entity == null ^ date == null) {
            if (this.entity == null) {
                this.message.append(" -> entity is null.");
            } else {
                this.message.append(" -> date is null.");
            }
            return false;
        }
        if (this.entity == null && date == null) {
            //TODO とりあえずどちらもnullの場合はfalseを返してメッセージを設定する
            this.message.append(" -> entity date both is null.");
            return false;
        }

        return this.field.is(this.entity, date);
    }

    /**
     * {@inheritDoc}
     * @see org.hamcrest.SelfDescribing#describeTo(org.hamcrest.Description)
     */
    public void describeTo(Description description) {
//        description.appendValue(this.entity);
        this.field.addDescription(description, this.entity);
        description.appendText(this.message.toString());
    }
}

ちょっと補足すると、DateMatcher#describeToでは、標準出力に出力する文字列を定義出来ます。
たとえばEclipseのJUnitでAssertionErrorが発生すると、以下のような出力が行われます。

java.lang.AssertionError: 
Expected: 
     got: 

description.appendValue(Object)でExpected:の値の部分に文字列が、
description.appendText(String)で上記に続く文字列が設定出来ます。

で、DateMatcherを作ったらテストクラスもあるわけで、以下の通りです。

import java.lang.reflect.Field;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

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

@RunWith(Parameterized.class)
public class DateMatcherTest {

    @Parameters
    public static Collection methods() {
        return Arrays.asList(new Object[][] {
                {null},
                {DateField.ALL},
                {DateField.YEAR},
                {DateField.MONTH},
                {DateField.DAY}
        });
    }
    private DateField field;
    
    public DateMatcherTest(DateField field) {
        this.field = field;
    }

    // 初期化の検証
    @Test
    public void コンストラクタの検証() throws Exception {
        DateMatcher matcher = new DateMatcher(new Date(), field);
        DateField  field = this.<DateField>getPrivateProperty(matcher, "field");
        assertThat(field, is(field));
    }
    @Test
    public void コンストラクタの検証_null() throws Exception {
        DateMatcher matcher = new DateMatcher(new Date(), null);
        DateField  field = this.<DateField>getPrivateProperty(matcher, "field");
        assertThat(field, is(DateField.ALL));
    }
    @Test
    public void isDateによるインスタンス生成() throws Exception {
        DateMatcher matcher = DateMatcher.isDate(new Date());
        DateField  field = this.<DateField>getPrivateProperty(matcher, "field");
        assertThat(field, is(DateField.ALL));
    }
    @Test
    public void isYearによるインスタンス生成() throws Exception {
        DateMatcher matcher = DateMatcher.isYear(2010);
        DateField  field = this.<DateField>getPrivateProperty(matcher, "field");
        assertThat(field, is(DateField.YEAR));
    }
    @Test
    public void isMonthによるインスタンス生成() throws Exception {
        DateMatcher matcher = DateMatcher.isMonth(11);
        DateField  field = this.<DateField>getPrivateProperty(matcher, "field");
        assertThat(field, is(DateField.MONTH));
    }
    @Test
    public void isDayによるインスタンス生成() throws Exception {
        DateMatcher matcher = DateMatcher.isDay(10);
        DateField  field = this.<DateField>getPrivateProperty(matcher, "field");
        assertThat(field, is(DateField.DAY));
    }
    @Test(expected=IllegalArgumentException.class)
    public void isMonth利用時の境界値の検証_MIN() {
        DateMatcher matcher = DateMatcher.isMonth(0);
    }
    @Test(expected=IllegalArgumentException.class)
    public void isMonth利用時の境界値の検証_OVER() {
        DateMatcher matcher = DateMatcher.isMonth(13);
    }
    @Test(expected=IllegalArgumentException.class)
    public void isDay利用時の境界値の検証_MIN() {
        DateMatcher matcher = DateMatcher.isMonth(0);
    }
    @Test(expected=IllegalArgumentException.class)
    public void isDay利用時の境界値の検証_OVER() {
        DateMatcher matcher = DateMatcher.isMonth(32);
    }
    
    
    // Matcherの検証
    @Test
    public void isDateの検証() {
        Date d = new Date();
        Date d1 = DateUtil.getDay(d);
        assertThat(d1, DateMatcher.isDate(d));
    }
    @Test
    public void isDateの検証_false() {
        Date d = new Date();
        Date d1 = DateUtil.getDay(DateUtil.moveDay(d, 1));
        assertThat(d1, not(DateMatcher.isDate(d)));
    }
    
    @Test
    public void isYearの検証_true() throws ParseException {
        Date d = DateFormater.toDate("2010/11/12");
        assertThat(d, DateMatcher.isYear(2010));
    }

    @Test
    public void isYearの検証_false() throws ParseException {
        Date d = DateFormater.toDate("2010/11/12");
        assertThat(d, not(DateMatcher.isYear(2011)));
    }

    @Test
    public void isMonthの検証_true() throws ParseException {
        Date d = DateFormater.toDate("2010/11/12");
        assertThat(d, DateMatcher.isMonth(11));
    }

    @Test
    public void isMonthの検証_false() throws ParseException {
        Date d = DateFormater.toDate("2010/11/12");
        assertThat(d, not(DateMatcher.isMonth(10)));
    }

    @Test
    public void isDayの検証_true() throws ParseException {
        Date d = DateFormater.toDate("2010/11/12");
        assertThat(d, DateMatcher.isDay(12));
    }

    @Test
    public void isDayの検証_false() throws ParseException {
        Date d = DateFormater.toDate("2010/11/12");
        assertThat(d, not(DateMatcher.isDay(11)));
    }
    @Test
    public void null値を含む検証0() {
        DateMatcher matcher = new DateMatcher(new Date(), null);
        assertThat(matcher.matches(null), is(false));
    }
    @Test
    public void null値を含む検証1() {
        DateMatcher matcher = new DateMatcher(null, null);
        assertThat(matcher.matches(new Date()), is(false));
    }
    @Test
    public void null値を含む検証2() {
        DateMatcher matcher = new DateMatcher(null, null);
        assertThat(matcher.matches(null), is(false));
    }
    
    
    // ユーティリティ
	// privateフィールドをリフレクションで取得する。
    public static <T> T getPrivateProperty(DateMatcher matcher, String propertyName) throws Exception {
        Field field = matcher.getClass().getDeclaredField(propertyName);
        field.setAccessible(true);
        return (T) field.get(matcher);
    }
}

良ければ使ってみてください。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

 
%d人のブロガーが「いいね」をつけました。