"こう動いてほしい"をテストに書く
プログラムは思ったとおりには動きません、作ったとおりに動きます。思ったとおりに動かすには、思ったとおりに作らなければなりません。Unit Test Frameworkは思ったとおりに作ることを手助けしてくれます。Test First/TDD(Test Driven Development:テスト駆動開発)では:
- こう動いてほしいをテストに書く
- テストに成功するように作り、テストを実行して検証する。
この2つを繰り返します。あなたの思いがもれなく/正しくテストに書かれ、そのすべてが成功すれば思ったとおりに作ったことになります。その後変更や拡張の必要が生じたら、そのたびにテストを加筆修正して思いをしたため、テスト結果がAll-Greenとなるまでコードをいじくることで常に思ったとおりに動く状態を維持します。テストコードにこう動いてほしいという思いをぶつけましょう。
#include "stdafx.h"
#include "CppUnitTest.h"
#include <orz/Date.h>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
using namespace orz;
namespace orzTest {
TEST_CLASS(DateTest) {
public:
TEST_METHOD(constructor) {
Date date(1,2,3);
Assert::AreEqual(1, date.year());
Assert::AreEqual(2, date.month());
Assert::AreEqual(3, date.day());
Date date2(date);
Assert::AreEqual(1, date2.year());
Assert::AreEqual(2, date2.month());
Assert::AreEqual(3, date2.day());
}
TEST_METHOD(copy) {
Date src(1,2,3);
Date date;
date = src;
Assert::AreEqual(1, date.year());
Assert::AreEqual(2, date.month());
Assert::AreEqual(3, date.day());
date = Date(4,5,6);
Assert::AreEqual(4, date.year());
Assert::AreEqual(5, date.month());
Assert::AreEqual(6, date.day());
}
// 一年未満はエラーになる
TEST_METHOD(bad_year) {
Date date;
Assert::IsFalse(date.set(0,1,1).is_valid());
}
// 一月から十二月までならよいけれど
TEST_METHOD(good_year_within) {
Date date;
Assert::IsTrue(date.set(2002, 1, 3).is_valid());
Assert::IsTrue(date.set(2003, 12, 4).is_valid());
}
// 一月未満はエラーだし
TEST_METHOD(bad_month_lt0) {
Date date;
Assert::IsFalse(date.set(2004, 0, 5).is_valid());
}
// 十二月を超えた場合もエラーだ
TEST_METHOD(bad_month_gt12) {
Date date;
Assert::IsFalse(date.set(2005,13, 6).is_valid());
}
// 同様に一日未満はエラーだ
TEST_METHOD(bad_day_lt0) {
Date date;
Assert::IsFalse(date.set(2006, 3, 0).is_valid());
}
// 小の月の三十日までや大の月の三十一日までならよいけれど
TEST_METHOD(goos_days_in_month) {
Date date;
Assert::IsTrue(date.set(2007, 4,30).is_valid());
Assert::IsTrue(date.set(2008, 3,31).is_valid());
}
// 小の月の三十日を超えた場合はエラーで
TEST_METHOD(bad_days_in_month_1) {
Date date;
Assert::IsFalse(date.set(2009, 9,31).is_valid());
}
// 大の月だって三十一日を超えちゃったらエラーなのだ
TEST_METHOD(bad_days_in_month_2) {
Date date;
Assert::IsFalse(date.set(2010, 8,32).is_valid());
}
// んでもって年が4で割り切れる年は原則うるう年なので二月二十九日は大丈夫で
TEST_METHOD(good_days_in_feb) {
Date date;
Assert::IsTrue(date.set(2004, 2,29).is_valid());
Assert::IsFalse(date.set(2005, 2, 29).is_valid());
}
// うるう年なら二月三十日はアウト
TEST_METHOD(bad_days_in_feb_on_leap) {
Date date;
Assert::IsFalse(date.set(2004, 2,30).is_valid());
}
// ただし年が100で割り切れちゃったらうるう年じゃない
TEST_METHOD(not_leap_on_div_100) {
Date date;
Assert::IsFalse(date.set(2100, 2, 29).is_valid());
}
// でも年が400で割り切れたらうるう年
TEST_METHOD(leap_on_div_400) {
Date date;
Assert::IsTrue(date.set(2000, 2, 29).is_valid());
Assert::IsTrue(date.set(2400, 2, 29).is_valid());
}
};
}
...これで十分でしょうか。単体テストエクスプローラからすべて実行すると、当然のことながら正しい日付を期待するテストはまっかっかです。
1つずつ、RedがGreenになるよう、Date.h/Date.cppに手を加えてはテストを繰り返してください。すべてのテストに成功すれば実装完了です。
テストの完了したコードを以下に示します。
#ifndef ORZ_DATE_H__
#define ORZ_DATE_H__
namespace orz {
class Date {
public:
Date();
Date(const Date&);
Date(int year, int month, int day);
Date& operator=(const Date&);
Date& set(int year, int month, int day);
int year() const;
int month() const;
int day() const;
bool is_valid() const; // 日付として正しい?
private:
bool valid_year() const; // 正しい年?
bool valid_month() const; // 正しい月?
bool valid_day() const; // 正しい日?
int days_in_month() const; // その月の日数
int days_in_feb() const; // その年の二月の日数
bool is_leap() const; // その年は閏年?
int year_;
int month_;
int day_;
};
}
#endif
#include <orz/Date.h>
namespace orz {
Date::Date() : year_(0), month_(0), day_(0) {}
Date::Date(int year, int month, int day)
: year_(year), month_(month), day_(day) {
}
Date::Date(const Date& date)
: year_(date.year_), month_(date.month_), day_(date.day_) {
}
Date& Date::operator=(const Date& date) {
if ( this != &date ) {
year_ = date.year_;
month_ = date.month_;
day_ = date.day_;
}
return *this;
}
Date& Date::set(int year, int month, int day) {
year_ = year;
month_ = month;
day_ = day;
return *this;
}
int Date::year() const { return year_; }
int Date::month() const { return month_; }
int Date::day() const { return day_; }
bool Date::is_valid() const {
// 年/月/日が正しければ日付として正しい
return valid_year() && valid_month() && valid_day();
}
bool Date::valid_year() const {
// 年は 1以上
return year_ >= 1;
}
bool Date::valid_month() const {
// 月は 1以上 12以下
return month_ >= 1 && month_ <= 12;
}
bool Date::valid_day() const {
// 日は 1以上 その月の日数以下
return day_ >= 1 && day_ <= days_in_month();
}
int Date::days_in_month() const {
// その月の日数
switch( month_ ) {
// 二月
case 2: return days_in_feb();
// 小の月
case 4: case 6: case 9: case 11: return 30;
// 大の月
default: return 31;
}
}
int Date::days_in_feb() const {
// 二月は 閏年なら29日 さもなくば 28日
return is_leap() ? 29 : 28;
}
bool Date::is_leap() const {
// 閏年は 4で割れて100で割れない または 400で割れる
return (year_ % 4 == 0 && year_ % 100 != 0)
|| (year_ % 400 == 0);
}
}

