Visual Studio 11(以下、VS11)の最大のウリはWindows 8流インターフェース・デザイン:Metro対応なのでしょうけど、根っからのC++屋な僕はそこらへんには目もくれず、Visual C++の新機能に興味津々です。Virtual PC上に用意したWindows 7へのVS11βインストール中に「What's New for Visual C++ in Visual Studio 11 Beta」を流し読みしていると、C++ unit test frameworkの文字が目に留まりました。
.NET(C#/VB.NET)であればVisual StudioがUnit Testプロジェクトの雛形を作ってくれ、Visual Studio内でテストできるのに、C++だけが置いてけぼりを喰らっていました。C++/CLIでテストを書く、あるいはWinUnitやCppUnitなどのUnit Test Frameworkをインストールすりゃいいのだけれどメンドクサイのは避けられない。C++に対応したUnit Test FrameworkがVisual Studioに組み込まれたのはマコトに喜ばしい限りです。追加インストール不要というお手軽さゆえにC++ Unit Test Frameworkの定番になるのかも。早速遊んでみましょかね。
同時にリリースされたVisual Studio 11 Express Beta for Windows 8もC++ Unit Test Frameworkに対応していました。
正しい日付を判定する
Unit Test Frameworkはその名のとおり単体テストのためのツールなのでテスト対象なしには話が始まりません。僕のオトモダチの一人、小島さんのアーティクル「ソフトウェア開発をシンプルにする考え方のコツ」から拝借させていただきます。
お題
namespace orz { class Date { public: Date(); Date& set(int year, int month, int day); bool is_valid() const; // 日付として正しいならtrueを返す private: int year_; int month_; int day_; ... }; }
コレを今回のお題としましょう。Visual Studio 11 Betaを立ち上げ、Win32 static libraryプロジェクト:Targetを起こし、お題に従いTarget/include/orz/Date.hとTarget/Date.cppを以下のように用意します:
#ifndef ORZ_DATE_H__ #define ORZ_DATE_H__ namespace orz { class Date { public: Date() : year_(1), month_(1), day_(1) {} Date(int year, int month, int day); Date& set(int year, int month, int day); int year() const { return year_; } int month() const { return month_; } int day() const { return day_; } bool is_valid() const; private: int year_; int month_; int day_; }; } #endif
#include <orz/Date.h> namespace orz { Date::Date(int year, int month, int day) : year_(year), month_(month), day_(day) { } Date& Date::set(int year, int month, int day) { year_ = year; month_ = month; day_ = day; return *this; } bool Date::is_valid() const { return false; } }
ここまではまだ前準備。コンパイルエラーとはならないものの中身はスカスカ。is_valid()はどんな日付であっても正しくないと判定します。
さてと、Unit Test Frameworkの出番です。新たにネイティブ単体テストプロジェクト:Testを起こします。
TestプロジェクトのプロパティでincludeパスとlibraryをTargetのしかるべき値に設定し、プロジェクト依存関係を指定します。
ひな形として用意されたunittest1.cppをDateTest.cppにリネームした後、以下のように修正します:
#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(set) { Date date; date.set(1,2,3); Assert::AreEqual(1, date.year()); Assert::AreEqual(2, date.month()); Assert::AreEqual(3, date.day()); } TEST_METHOD(good_date) { Date date; Assert::IsTrue(date.set(2012,3,31).is_valid()); } }; }
テスト・コード(ここではDateTest.cpp)にはいくつかのおやくそくがあります。
TEST_CLASS(class_name) { ... };
後述するTEST_METHODをいくつかまとめたものがTEST_CLASSです。テスト対象ごとにTEST_CLASSを定義するのが標準と言えるでしょうが、シチュエーションあるいはお好みに応じて(例えば正常系と異常系とか)細分化しても構いません。その際は一本のソースに複数のTEST_CLASSを書き並べてもいいし、追加/新しい項目/C++単体テストクラスで別ソースに分けるもよし。
TEST_METHOD(method_name) { ... }
テストの各項目に相当します。この中でテスト対象を動かし、その結果を検証します(検証方法は後述)。
上記2つはお決まりのルールとして、省略可能なものが2×3=6つほど:
TEST_MODULE_INITIALIZE(method_name) { ... }
TEST_MODULE_CLEANUP(method_name) { ... }
全テストの実行直前/直後にそれぞれ一度だけ呼び出されます。その名のとおり、テスト・モジュールの前準備/後始末を行うためのもの。環境変数/グローバル変数を設定したりDLLをLoadLibarary/FreeLibraryしたりCOMを初期化/解放したり。
TEST_CLASS_INITIALIZE(method_name) { ... }
TEST_CLASS_CLEANUP(method_name) { ... }
TEST_CLASS内の一連のTEST_METHOD実行直前/直後に一度だけ行う前準備/後始末を定義します。例えばデータベースやSocketとの接続/切断だとか。
TEST_METHOD_INITIALIZE(method_name) { ... }
TEST_METHOD_CLEANUP(method_name) { ... }
各TEST_METHODの実行直前/直後に呼び出されます。テストごとの前準備/後始末を行うのが目的です。
Logger::WriteMessage()を埋め込んだ(空の)テスト:
#include "stdafx.h" #include "CppUnitTest.h" using namespace Microsoft::VisualStudio::CppUnitTestFramework; namespace dummy { TEST_MODULE_INITIALIZE(test_module_initialize) { Logger::WriteMessage(__FUNCTION__); } TEST_MODULE_CLEANUP(test_module_cleanup) { Logger::WriteMessage(__FUNCTION__); } TEST_CLASS(DummyTest) { public: TEST_CLASS_INITIALIZE(test_class_initialize) { Logger::WriteMessage(__FUNCTION__); } TEST_CLASS_CLEANUP(test_class_cleanup) { Logger::WriteMessage(__FUNCTION__); } TEST_METHOD_INITIALIZE(test_method_initialize) { Logger::WriteMessage(__FUNCTION__); } TEST_METHOD_CLEANUP(test_method_cleanup) { Logger::WriteMessage(__FUNCTION__); } TEST_METHOD(TestMethod1) { Logger::WriteMessage(__FUNCTION__); } TEST_METHOD(TestMethod2) { Logger::WriteMessage(__FUNCTION__); } TEST_METHOD(TestMethod3) { Logger::WriteMessage(__FUNCTION__); } }; }
これを実行すると以下の結果が得られます。
TEST_METHODの実行順序はTEST_CLASSに並べた順にはならないみたいなので、実行順序に依存するテストは御法度ですね。
今回はINITIALIZE/CLEANUPの必要はありませんからばっさり省略。ひとまずこの段階でテストを実行してみましょう。メニューから[表示]-[その他のウィンドウ]-[単体テストエクスプローラ]を開いてすべて実行すると、テスト対象とテストがビルドされた後、実行結果が表示されます。