ObjectSquare [2001 年 10 月号]

[技術講座] CppUnit 入門


5. テスト作成テクニック

5.1. Dice のテスト − RepeatedTest

 続いて Dice のテストを書こうと思いますが、サイコロの目はランダムに生成されるので、結果を決定論的に書くことができません。とりあえず目が1以上6以下であるかチェックすることにしましょう。

Dice dice;
dice.shuffle();
int value = dice.getValue();
TEST_ASSERT(1 <= value && value <= 6);

 しかし、1回チェックしただけでは、たまたまその範囲に収まっただけかもしれません。できれば何回かチェックしたくなります。そういうときに便利なのが、CppUnit の拡張機能 RepeatedTest です。

dicegame/tests/DiceTest.cpp
#include "DiceTest.h"
#include "dicegame/Dice.h"
#include <cppunit/TestSuite.h>
#include <cppunit/TestCaller.h>
#include <cppunit/RepeatedTest.h>

USING_NAMESPACE_CPPUNIT

void DiceTest::testDice()
{
    /* 省略 */
}

void DiceTest::testLimits()
{
    Dice dice;
    dice.shuffle();
    int value = dice.getValue();
    TEST_ASSERT(1 <= value && value <= 6);
}

Test* DiceTest::suite()
{
    TestSuite* suite = new TestSuite("DiceTest");
    suite->addTest(new TEST_CALLER(DiceTest, testDice));
    /* DiceTest::testLimits() を12回実行する。 */
    suite->addTest(new RepeatedTest(new TEST_CALLER(DiceTest, testLimits), 12));
    return suite;
}

5.2. DiceGame のテスト − スタブの利用

 次はコントローラクラス DiceGame のテストです。DiceGame は他のクラスと密接に関連しているので、それらをまとめてテストすることにします。しかしながら、Dice、DiceGamePlayer は作成 & テスト済みですが、ユーザインタフェースはまだできていません。そこで、テスト用のスタブクラスを用意することにします。ここで、設計時にインタフェース DiceGameListener を用意しておいたのが有効に働きます。

dicegame/DiceGameListener.h
#ifndef dicegame_DiceGameListener_h
#define dicegame_DiceGameListener_h

class DiceGameListener
{
  public:
    enum OddOrEvenReply { ODD  = 1, EVEN = 2, STOP = 9 };

  public:
    virtual ~DiceGameListener() {}
    virtual void startRound() = 0;
    virtual void endRound() = 0;
    virtual int inquireOddOrEven() = 0;
};

#endif /* !dicegame_DiceGameListener_h */

 テストに使いやすいように DiceGameListener のスタブクラスを定義します。例えばこんな感じでどうでしょう。

dicegame/tests/DiceGameTest.h
#ifndef dicegame_tests_DiceGameTest_h
#define dicegame_tests_DiceGameTest_h

#include "dicegame/DiceGameListener.h"
#include <cppunit/TestCase.h>

USING_NAMESPACE_CPPUNIT

class DiceGameTest : public TestCase
{
  public:
    explicit DiceGameTest(const char* name) : TestCase(name) {}
    void testCreate();
    void testInquireOddOrEven();
    void testJudge();
    void testDoRound();
    void testRun();

    static Test* suite();

  public:
    /* テスト用スタブクラス */
    class TestListener : public DiceGameListener {
      public:
        TestListener() : started(0), ended(0), inquired(0) {}
        virtual void startRound() { ++started; }
        virtual void endRound() { ++ended; }
        virtual int inquireOddOrEven();
        int started, ended, inquired;
    };
};

#endif /* !dicegame_tests_DiceGameTest_h */

dicegame/tests/DiceGameTest.cpp
#include "DiceGameTest.h"
#include "dicegame/DiceGamePlayer.h"
#include "dicegame/DiceGame.h"
#include <cppunit/TestSuite.h>
#include <cppunit/TestCaller.h>
#include <cppunit/RepeatedTest.h>

USING_NAMESPACE_CPPUNIT

int DiceGameTest::TestListener::inquireOddOrEven()
{
    ++inquired;
    return (inquired < 5) ? inquired : STOP;
}

void DiceGameTest::testCreate()
{
    /* 省略 */
}

void DiceGameTest::testInquireOddOrEven()
{
    /* 省略 */
}

void DiceGameTest::testJudge()
{
    /* 省略 */
}

void DiceGameTest::testDoRound()
{
    /* 省略 */
}

void DiceGameTest::testRun()
{
    /* TestListener を使って DiceGame を生成。 */
    TestListener listener;
    DiceGame diceGame(&listener);

    /* TestListener の各属性が初期化されているか? */
    TEST_ASSERT_EQUALS(0, listener.started);
    TEST_ASSERT_EQUALS(0, listener.ended);
    TEST_ASSERT_EQUALS(0, listener.inquired);

    diceGame.run();

    /* TestListener::inquireOddOrEven() の戻り値は、
     *   1回目: ODD
     *   2回目: EVEN
     *   3回目: 不正値
     *   4回目: 不正値
     *   5回目: STOP
     * となるので、こういう結果になるはず。 */
    TEST_ASSERT_EQUALS(2, listener.started);
    TEST_ASSERT_EQUALS(2, listener.ended);
    TEST_ASSERT_EQUALS(5, listener.inquired);
    TEST_ASSERT_EQUALS(2, diceGame.getRound());
    DiceGamePlayer* player = diceGame.getPlayer();
    TEST_ASSERT_EQUALS(2, player->getWins() + player->getLosses());
}

Test* DiceGameTest::suite()
{
    TestSuite* suite = new TestSuite("DiceGameTest");
    suite->addTest(new TEST_CALLER(DiceGameTest, testCreate));
    suite->addTest(new TEST_CALLER(DiceGameTest, testInquireOddOrEven));
    suite->addTest(new TEST_CALLER(DiceGameTest, testJudge));
    suite->addTest(new RepeatedTest(
            new TEST_CALLER(DiceGameTest, testDoRound), 4));
    suite->addTest(new TEST_CALLER(DiceGameTest, testRun));
    return suite;
}

 ユーザの入力を、テスト用スタブクラスでシミュレートしながらテストを書いていくわけです。

5.3. DiceGameUI のテスト

 最後に DiceGameUI ですが、今回このクラスの単体テストはしないことにします。キャラクタベースなら入出力を std::istreamstd::ostream にしておいて std::stringstream を使ってテストを書くということも可能ですが、GUI ですとテストが書きにくく、またユーザインタフェースは仕様も変化しやすいので、ロジック部分をできるだけ下位のクラスに移して、そちらを重点的にテストするようにした方が効果的な気がします。
 ユーザインタフェースのテストは受け入れテストで行うことにしましょう。


© 2001 OGIS-RI Co., Ltd.
Prev. Index Next
Prev. Index Next