目的
TDDブートキャンプというセミナーに参加して面白かったので、
自分で何かやってみようと思った。
仕様
日付期間を保持するオブジェクトである。
開始日と終了日を持つ。
開始日と終了日を持つことをどうやってテストするかということが、
セミナー内でも少し話題に上がっていました。
よくやるパターンというのが、
「値をもっているならtoStringメソッドで出力させて、その結果と等価判定する」
というものだったので、それを実際にテストすると以下のようになります。
<テストコード>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
String start_20200801 = "20200801"; String end_20200831 = "20200831"; @Test void 開始日と終了日を持つ() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Period period = new Period(start, end); assertThat(period.toString(), is("[2020-08-01 - 2020-08-31]")); } private Date stringToDate(String dateStr, SimpleDateFormat sdformat) { Date date = null; try { date = sdformat.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } return date; } |
・String型の日付(start_20200801等)は、共通で使用できるようにTestClassのフィールドとして保持。
・stringToDateメソッドは、日付を返す共通メソッド。
<実コード>
1 2 3 4 5 6 7 8 9 10 11 |
public class Period { private Date _start; private Date _end; public String toString() { String format = "yyyy-MM-dd"; return String.format("[%s - %s]", DateToString(_start, format), DateToString(_end, format)); } } |
終了日は開始日を含む未来日である。
この日付期間オブジェクトは開始日と終了日を持つのですが、普通に考えると終了日は開始日より未来にあるべきだということになります。
そして、開始日と終了日が同じということはあり得るので、終了日は開始日を含んだ未来の日付として保持されるという仕様にしました。
<テストコード>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
@Nested class 終了日は開始日を含む未来日である{ @Test void 日付期間は開始日_2020_08_01_と終了日_2020_08_01_を保持できる() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200801, sdformat); Period period = new Period(start, end); assertThat(period.toString(), is("[2020-08-01 - 2020-08-01]")); } @Test void 日付期間は開始日_2020_08_01_と終了日_2020_08_02_を保持できる() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200802, sdformat); Period period = new Period(start, end); assertThat(period.toString(), is("[2020-08-01 - 2020-08-02]")); } @Test void 日付期間は開始日_2020_08_02_と終了日_2020_08_01_を保持できない() { start = stringToDate(start_20200802, sdformat); end = stringToDate(end_20200801, sdformat); assertThrows(IllegalArgumentException.class, () -> new Period(start, end)); } } |
<実コード>
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Period { public Period(Date start, Date end) { if(end.before(start)) { throw new IllegalArgumentException( "Start :" + start + " - End :" + end + " -> 終了日は開始日を含む未来日として下さい。"); } _start = start; _end = end; } } |
テストコードで@Nestedアノテーションをclassにつけてメソッドを中にまとめることで、テスト結果をネストさせて表現できるようになります。
ある日付期間と等価であること判定する。
この日付期間オブジェクトは、別の日付期間オブジェクトと等価であるかを判定できるとします。
判定は、自オブジェクトの開始日/終了日と、別の日付期間オブジェクトの開始日/終了日が共に同日であれば等価とみなします。
<テストコード>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
@Nested class 日付期間が等価であること判定できる { @Test void 開始日_2020_08_01_と終了日_2020_08_31_は開始日_2020_08_01_と終了日_2020_08_31_と一致する(){ start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); otherStart = stringToDate(start_20200801, sdformat); otherEnd = stringToDate(end_20200831, sdformat); Period period = new Period(start, end); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.equals(otherPeriod), is(true)); } @Test void 開始日_2020_08_01_と終了日_2020_08_31_は開始日_2020_08_01_と終了日_2020_08_30_と一致しない(){ start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); otherStart = stringToDate(start_20200801, sdformat); otherEnd = stringToDate(end_20200830, sdformat); Period period = new Period(start, end); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.equals(otherPeriod), is(false)); } @Test void 開始日_2020_08_01_と終了日_2020_08_31_は開始日_2020_07_31_と終了日_2020_08_31_と一致しない(){ start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); otherStart = stringToDate(start_20200731, sdformat); otherEnd = stringToDate(end_20200831, sdformat); Period period = new Period(start, end); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.equals(otherPeriod), is(false)); } } |
<実コード>
1 2 3 4 5 6 7 |
public class Period { public boolean equals(Period other) { return _start.equals(other._start) && _end.equals(other._end); } } |
ある日付が日付期間の範囲内であることを判定する
日付期間オブジェクトが、ある特定の日付を範囲内に含むんでいるかを判定できるようにします。
ここでは、開始日を含む以降であり、終了日を含む以前としました。
<テストコード>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
@Nested class ある日付が日付期間の範囲内であることを判定できる { @Test void 開始日_2020_08_01_と終了日_2020_08_31_において日付_2020_08_01_は範囲内である() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Date _20200801 = stringToDate("20200801", sdformat); Period period = new Period(start, end); assertThat(period.contains(_20200801), is(true)); } @Test void 開始日_2020_08_01_と終了日_2020_08_31_において日付_2020_08_31_は範囲内である() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Date _20200831 = stringToDate("20200831", sdformat); Period period = new Period(start, end); assertThat(period.contains(_20200831), is(true)); } @Test void 開始日_2020_08_01_と終了日_2020_08_31_において日付_2020_08_15_は範囲内である() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Date _20200815 = stringToDate("20200815", sdformat); Period period = new Period(start, end); assertThat(period.contains(_20200815), is(true)); } @Test void 開始日_2020_08_01_と終了日_2020_08_31_において日付_2020_07_31_は範囲外である() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Date _20200731 = stringToDate("20200731", sdformat); Period period = new Period(start, end); assertThat(period.contains(_20200731), is(false)); } @Test void 開始日_2020_08_01_と終了日_2020_08_31_において日付_2020_09_01_は範囲外である() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Date _20200901 = stringToDate("20200901", sdformat); Period period = new Period(start, end); assertThat(period.contains(_20200901), is(false)); } } |
<実コード>
1 2 3 4 5 6 7 8 9 |
public class Period { public boolean contains(Date when) { boolean isAfterIncludingStart = _start.equals(when) || _start.before(when); boolean isBeforeIncludingEnd = _end.after(when) || _end.equals(when); return isAfterIncludingStart && isBeforeIncludingEnd; } } |
ある日付期間が全て含まれていることを判定する
日付期間オブジェクトが、別の日付期間オブジェクトの開始日/終了日を完全に内包していることを判定できるようにします。
言葉で表現すると解釈に違いがでてしまう可能性がありますが、TDDでは値で表現してしまえるのが良いところです。
<テストコード>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
@Nested class ある日付期間が全て含まれていることを判定できる { @Test void 日付期間_2020_08_01__2020_08_31_は_ある日付期間_2020_08_01__2020_08_31_を全て含むと判定する() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); otherStart = stringToDate(start_20200801, sdformat); otherEnd = stringToDate(end_20200831, sdformat); Period period = new Period(start, end); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.contains(otherPeriod), is(true)); } @Test void 日付期間_2020_08_01__2020_08_31_は_ある日付期間_2020_08_02__2020_08_30_を全て含むと判定する() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); otherStart = stringToDate(start_20200802, sdformat); otherEnd = stringToDate(end_20200830, sdformat); Period period = new Period(start, end); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.contains(otherPeriod), is(true)); } @Test void 日付期間_2020_08_01__2020_08_31_は_ある日付期間_2020_07_31__2020_08_01_を全て含まないと判定する() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); otherStart = stringToDate(start_20200731, sdformat); otherEnd = stringToDate(end_20200801, sdformat); Period period = new Period(start, end); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.contains(otherPeriod), is(false)); } @Test void 日付期間_2020_08_01__2020_08_31_は_ある日付期間_2020_08_31__2020_09_01_を全て含まないと判定する() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); otherStart = stringToDate(start_20200831, sdformat); otherEnd = stringToDate(end_20200901, sdformat); Period period = new Period(start, end); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.contains(otherPeriod), is(false)); } } |
<実コード>
1 2 3 4 5 6 7 |
public class Period { public boolean contains(Period other) { return contains(other._start) && contains(other._end); } } |
開始日と終了日が共に範囲内であれば、完全に内包していると言えることをコードでうまく表現できています。
ある日付期間の一部が含まれていることを判定する
これは思い付きで考えた機能で、日付期間の部分一致のようなものです。
日付期間オブジェクト同士で、重なっている日付があるかを判定します。
<テストコード>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
@Nested class ある日付期間の一部が含まれていることを判定できる { @Test void 日付期間_2020_08_01__2020_08_31_は_ある日付期間_2020_07_31__2020_08_01_を含むと判定する() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Period period = new Period(start, end); otherStart = stringToDate(start_20200731, sdformat); otherEnd = stringToDate(end_20200801, sdformat); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.containsPart(otherPeriod), is(true)); } @Test void 日付期間_2020_08_01__2020_08_31_は_ある日付期間_2020_08_31__2020_09_01_を含むと判定する() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Period period = new Period(start, end); otherStart = stringToDate(start_20200831, sdformat); otherEnd = stringToDate(end_20200901, sdformat); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.containsPart(otherPeriod), is(true)); } @Test void 日付期間_2020_08_01__2020_08_31_は_ある日付期間_2020_08_01__2020_08_31_を含むと判定する() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Period period = new Period(start, end); otherStart = stringToDate(start_20200801, sdformat); otherEnd = stringToDate(end_20200831, sdformat); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.containsPart(otherPeriod), is(true)); } @Test void 日付期間_2020_08_01__2020_08_31_は_ある日付期間_2020_07_31__2020_07_31_を含まないと判定する() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Period period = new Period(start, end); otherStart = stringToDate(start_20200731, sdformat); otherEnd = stringToDate(end_20200731, sdformat); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.containsPart(otherPeriod), is(false)); } @Test void 日付期間_2020_08_01__2020_08_31_は_ある日付期間_2020_09_01__2020_09_01_を含まないと判定する() { start = stringToDate(start_20200801, sdformat); end = stringToDate(end_20200831, sdformat); Period period = new Period(start, end); otherStart = stringToDate(start_20200901, sdformat); otherEnd = stringToDate(end_20200901, sdformat); Period otherPeriod = new Period(otherStart, otherEnd); assertThat(period.containsPart(otherPeriod), is(false)); } } |
<実コード>
1 2 3 4 5 6 7 8 9 |
public class Period { public boolean containsPart(Period other) { if(_start.after(other._end)) return false; if(_end.before(other._start)) return false; return true; } } |
ガード節にすることでわかり易く表現できています。
このように、完全に外れていたらFalseを返し、そうでなければ一部を含んでいるはずなのでTrueが返るようにしています。
実行結果
Greenですね♪
ソースコード ダウンロード
Githubに公開しています。
https://github.com/TakumiKondo/parts/tree/master/src/period
TDDを実践してみてわかったこと
・テストコードを先に書くので、仕様を明確にする習慣が身に付く。
・実コードが正しいかについて確信が持てる。
・コードの変更(リファクタリング含め)が怖くなくなる。