"こう動いてほしい"をテストに書く
プログラムは思ったとおりには動きません、作ったとおりに動きます。思ったとおりに動かすには、思ったとおりに作らなければなりません。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); } }