ObjectSquare [2001 年 10 月号]

[技術講座] CppUnit 入門


4. はじめの一歩

4.1. DiceGamePlayer の準備

 お待たせしました。それではいよいよ 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

4.2. DiceGamePlayerTest の作成

 それでは 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引数にチェックしたい変数を指定します。今のところ intlongdoublechar* (C 形式の文字列) に対応しています。

TEST_ASSERT_EQUALS(0, player.getWins());  // … (a)
TEST_ASSERT(player.getWins() == 0);  // … (b)
のように書くこともできますが、(a) の形式で書くことによって、変数に実際に入っていた値を見ることができます。なお、オリジナルの CppUnit ではマクロ関数の名前が 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 を使うことで、
new TEST_CALLER(DiceGamePlayerTest, testCreate);
と簡単に記述することができるようにしました。第1引数がクラス名、第2引数がメンバ関数名です。

 テストが作成できたら 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です。

Project2

 ここまでに作成したファイルをまとめたものを用意しました。実際に実行してテストが失敗することを確かめてみてください。

4.3. DiceGamePlayer の実装

 それでは、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.
Prev. Index Next
Prev. Index Next