[技術講座] CppUnit 入門
お待たせしました。それではいよいよ CppUnit を使ってテストを書いてみます。DiceGamePlayer クラスから始めます。テストから先に作るのが XP の流儀 (テストファースト) ですが、コンパイルが通るところまではクラスの雛型を作ってしまいましょう。ただし、メンバ関数の実装はできるだけここでは書かないようにします。
dicegame/DiceGamePlayer.h
#ifndef dicegame_DiceGamePlayer_h #define dicegame_DiceGamePlayer_h #include <string> class DiceGamePlayer { public: DiceGamePlayer(const std::string& name = ""); std::string getName() const { return m_name; } int getWins() const { return m_wins; } int getLosses() const { return m_losses; } void winGame(); void loseGame(); private: std::string m_name; int m_wins; int m_losses; }; #endif /* !dicegame_DiceGamePlayer_h */
dicegame/DiceGamePlayer.cpp
#include "DiceGamePlayer.h" DiceGamePlayer::DiceGamePlayer(const std::string& name) { /* 実装はまだ書かない */ } void DiceGamePlayer::winGame() { /* 実装はまだ書かない */ } void DiceGamePlayer::loseGame() { /* 実装はまだ書かない */ }
ここまでできたら、ソースファイルをプロジェクトに追加します。Automake を使う場合は dicegame/Makefile.am を次のように書き換えます。
dicegame/Makefile.am
SUBDIRS = . tests noinst_LIBRARIES = libdicegame.a # libdicegame.a のソースファイル。 libdicegame_a_SOURCES = \ DiceGamePlayer.cpp \ DiceGamePlayer.h
それでは DiceGamePlayer クラスのテストを作成します。テストの名前は、テストしたいクラスのクラス名の後に Test をつけて DiceGamePlayerTest とします。
dicegame/tests/DiceGamePlayerTest.h
#ifndef dicegame_tests_DiceGamePlayerTest_h #define dicegame_tests_DiceGamePlayerTest_h #include <cppunit/TestCase.h> USING_NAMESPACE_CPPUNIT /* テストクラスは TestCase を継承する。 */ class DiceGamePlayerTest : public TestCase { public: /* お決まりのコンストラクタ。 */ explicit DiceGamePlayerTest(const char* name) : TestCase(name) {} /* テストは testXXX という名前にする。 */ void testCreate(); void testWinGame(); void testLoseGame(); /* このクラスのテストをまとめて返す static メンバ関数。 */ static Test* suite(); }; #endif /* !dicegame_tests_DiceGamePlayerTest_h */
オリジナルの CppUnit では、コンストラクタの引数は std::string
ですが、string
クラスが使えない環境を考慮して、私の改良版では C 形式の文字列に変更しています。
dicegame/tests/DiceGamePlayerTest.cpp
#include "DiceGamePlayerTest.h" #include "dicegame/DiceGamePlayer.h" #include <cppunit/TestSuite.h> #include <cppunit/TestCaller.h> USING_NAMESPACE_CPPUNIT void DiceGamePlayerTest::testCreate() { DiceGamePlayer player; /* 各属性が初期化されているか? */ TEST_ASSERT_EQUALS("", player.getName().c_str()); TEST_ASSERT_EQUALS(0, player.getWins()); /* 以下省略 */ } void DiceGamePlayerTest::testWinGame() { DiceGamePlayer player; /* wins の初期値は 0。 */ TEST_ASSERT_EQUALS(0, player.getWins()); /* winGame() を呼ぶごとに wins の値が1ずつ増える。 */ player.winGame(); TEST_ASSERT_EQUALS(1, player.getWins()); player.winGame(); TEST_ASSERT_EQUALS(2, player.getWins()); } void DiceGamePlayerTest::testLoseGame() { /* 省略 */ } Test* DiceGamePlayerTest::suite() { /* TestSuite を生成して、各テストを追加する。 */ TestSuite* suite = new TestSuite("DiceGamePlayerTest"); suite->addTest(new TEST_CALLER(DiceGamePlayerTest, testCreate)); suite->addTest(new TEST_CALLER(DiceGamePlayerTest, testWinGame)); suite->addTest(new TEST_CALLER(DiceGamePlayerTest, testLoseGame)); return suite; }
ソースコードを見てもらえればわかるように、各テストでは TEST_ASSERT_EQUALS()
マクロ関数を使って値のチェックをします。第1引数に期待される値、第2引数にチェックしたい変数を指定します。今のところ int
、long
、double
、char*
(C 形式の文字列) に対応しています。
はTEST_ASSERT_EQUALS(0, player.getWins()); // … (a)
のように書くこともできますが、(a) の形式で書くことによって、変数に実際に入っていた値を見ることができます。なお、オリジナルの CppUnit ではマクロ関数の名前がTEST_ASSERT(player.getWins() == 0); // … (b)
assert()
になっていますが、<assert.h> で定義されている標準マクロ関数とのバッティング回避と、マクロであることを強調するために、私の改良版では名前を TEST_ASSERT()
に変更しています。オリジナルの CppUnit と同じ assert()
を使いたい場合は、<cppunit/TestCase.h> をインクルードする前にマクロ CPPUNIT_COMPATIBLE
を define してください。
また、suite()
では複数のテストをまとめる TestSuite オブジェクトを生成して、各テストを追加しています。その際、メンバ関数ポインタをオブジェクトに変換するテンプレートクラス TestCaller を使用しています。これも、オリジナルの CppUnit では、
と書かないといけないところを、私の改良版ではマクロnew TestCaller<DiceGamePlayerTest>("DiceGamePlayerTest::testCreate", DiceGamePlayerTest::testCreate);
TEST_CALLER
を使うことで、
と簡単に記述することができるようにしました。第1引数がクラス名、第2引数がメンバ関数名です。new TEST_CALLER(DiceGamePlayerTest, testCreate);
テストが作成できたら AllTest.cpp にこのテストを追加します。
dicegame/tests/AllTests.cpp
#include "DiceGamePlayerTest.h" #include <cppunit/TestRunner.h> int main(int argc, char* argv[]) { /* TestRunner を生成して run() を実行する。 */ CPPUNIT::TestRunner runner; /* ここに今後テストを追加していく。 */ runner.addTest("DiceGamePlayerTest", DiceGamePlayerTest::suite()); return runner.run(argc, argv); }
Makefile.am にもソースファイルを追加します。
dicegame/tests/Makefile.am
TESTS = AllTests INCLUDES = -I$(top_srcdir) -I$(includedir) noinst_PROGRAMS = AllTests # AllTests(.exe) のソースファイル。 AllTests_SOURCES = \ AllTests.cpp \ DiceGamePlayerTest.cpp \ DiceGamePlayerTest.h AllTests_LDFLAGS = -L$(libdir) AllTests_LDADD = ../libdicegame.a -lcppunit
それでは動かしてみましょう。まず Makefile を作り直します。
$ ./bootstrap $ ./configure
続いてビルドを行います。
$ make
エラーが発生したら、ファイルを修正して初めからやり直してください。ソースコードのエラーであれば make だけ実行すれば十分です。
$ dicegame/tests/AllTests .F .F .F !!!FAILURES!!! Test Results: Run: 3, Failures: 3, Errors: 0 There were 3 failures: 1) DiceGamePlayerTest.cpp(17): expected:<0> but was:<39647804> 2) DiceGamePlayerTest.cpp(29): expected:<1> but was:<0> 3) DiceGamePlayerTest.cpp(37): expected:<0> but was:<39647804>
テストを実行すると、今回追加した3つのテスト全てが失敗しています。DiceGamePlayer.cpp をまだ実装していないので当たり前ですが、このように まずいったんテストを失敗させる のがポイントです。これで、テストがちゃんと機能していることが確かめられます。
MSVC++ の場合は、次のように各ファイルをプロジェクトに追加してビルド、実行すればOKです。
ここまでに作成したファイルをまとめたものを用意しました。実際に実行してテストが失敗することを確かめてみてください。
それでは、DiceGamePlayer を実装しましょう。
dicegame/DiceGamePlayer.cpp
#include "DiceGamePlayer.h" DiceGamePlayer::DiceGamePlayer(const std::string& name) : m_name(name), m_wins(0), m_losses(0) { } void DiceGamePlayer::winGame() { ++m_wins; } void DiceGamePlayer::loseGame() { ++m_losses; } |
こんな感じでしょうか。ビルド、実行して全てのテストが成功すればテスト完了です。
$ make $ dicegame/tests/AllTests ... OK (3 tests)
テストが失敗した場合は、残念、失敗した箇所を調べて修正し、成功するまでテストをやり直してください。
《参考文献・URL》
© 2001 OGIS-RI Co., Ltd. |
|