C++用xUnitでのテストの書き方 - 2009-11-07 - ククログ

ククログ

株式会社クリアコード > ククログ > C++用xUnitでのテストの書き方

C++用xUnitでのテストの書き方

注: 長いです。

スクリプト言語でのxUnit実装を使ったことがある方なら、テストを定義するだけでテストが実行されることが当たり前ではないでしょうか。c2.comのWikiによると、これはTest Collectorというそうです。定義したテストを自動的に集めてくる機能のことです。

一般的にTest Collectorの機能は言語が提供するリフレクション機能やメタプログラミング機能を使って実現されます。

例えば、Rubyのtest-unit 2.xでは、リフレクションを使う方法とメタプログラミングを使う方法の両方をサポートしています。リフレクションを使う方法ではObjectSpace.each_object(Class)ですべてのクラスを取得し、その中のTest::Unit::TestCaseのサブクラスを集めます。メタプログラミングを使う方法ではTest::Unit::TestCase.inheritedを定義して、サブクラスが定義された時のフックでそのサブクラスを集めます。

Pythonでもモジュールオブジェクトからモジュール内のオブジェクトにアクセスすることができるので、同様の方法でテストを集めることができます。JavaScriptでも、オブジェクトに定義されているプロパティを列挙することができるので、同様の方法でテストを集めることができます。(JavaScriptとXULで実装されているUxUも同様のことをしています。)

一方、CやC++ではリフレクション機能やクラスをファーストクラスオブジェクトとして扱う機能がないため、自動でテストを集めるためには一工夫必要になります。一昔前のxUnitでは、定義したテストを手動で登録していました。

それでは、C/C++での一工夫の方法として以下の4つをxUnit実装と一緒に紹介します。C++用のxUnitを選択する時の参考にしてください。

  • マクロでごまかす
  • プリコンパイルする
  • マクロでテスト定義と同時に登録する
  • 共有ライブラリから探す

マクロでごまかす

最初はCppUnitのケースです。CppUnitではテストを定義するだけでは、自動でテストを集めてはくれません。しかし、便利マクロを用意して、手動でテストを登録する面倒さを減らしています。

以下はCppUnit Cookbookにあるソースコードをベースにしています。public内のテスト定義とは別にCPPUNIT_TEST_SUITEからCPPUNIT_TEST_SUITE_ENDの間でテストを登録しています。

// complex-number-test.cpp
#include <cppunit/extensions/HelperMacros.h>

class ComplexNumberTest : public CppUnit::TestFixture  {
  CPPUNIT_TEST_SUITE( ComplexNumberTest );
  CPPUNIT_TEST( testEquality );
  CPPUNIT_TEST( testAddition );
  CPPUNIT_TEST_SUITE_END();

private:
  Complex *m_10_1, *m_1_1, *m_11_2;

public:
  void setUp()
  {
     m_10_1 = new Complex( 10, 1 );
     m_1_1 = new Complex( 1, 1 );
     m_11_2 = new Complex( 11, 2 );
  }

  void tearDown()
  {
    delete m_10_1;
    delete m_1_1;
    delete m_11_2;
  }

  void testEquality()
  {
    CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
    CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
  }

  void testAddition()
  {
    CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
  }
};
CPPUNIT_TEST_SUITE_REGISTRATION( ComplexNumberTest );

実行する場合は以下のようなmain関数を定義する必要があります。

// main.cpp
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>

int main( int argc, char **argv)
{
  CppUnit::TextUi::TestRunner runner;
  CppUnit::TestFactoryRegistry &registry =
    CppUnit::TestFactoryRegistry::getRegistry();
  runner.addTest( registry.makeTest() );
  bool wasSuccessful = runner.run( "", false );
  return wasSuccessful;
}

上記の二つのファイル(とComplexNumberの実装)を使ってビルドします。

% g++ -o complex-number-test complex-number-test.cpp main.cpp \
    -lcomplex-number -lcppunit

テストを実行するにはビルドしたバイナリを実行します。

% ./complex-number-test
..


OK (2 tests)

マクロを使ってテスト登録を簡単にしている(自動化まではしていない)例としてCppUnitを紹介しました。

CPPUNIT_TEST_SUITEなどの便利マクロを使わない場合は定義したテスト名(上の例ではtestEqualitytestAddition)以外のことも気にしなければいけなくなります。便利マクロを使うと、テスト名だけわかっていればよいので、それに比べるとだいぶテスト作成が楽になっています。

しかし、テストを定義だけして登録し忘れたということを回避することができません。また、テストケース定義とは別にmain関数も定義する必要があり、テスト以外のことにも気を配る必要があることにも注意が必要です。

プリコンパイルする

C++で書かれたテストコードを直接C++コンパイラでコンパイルするのではなく、テストコードに必要なコードを追加したC++ソースコードを生成して、それをコンパイルする方法です。C++コンパイラでのビルドする前に一度変換処理を行えるので、テストコードへの記述が減ることが利点ですが、変換処理を行うのがやや面倒です。自動化されれば気にならなくなるでしょう。

CxxTest

まずはCxxTestのケースです。CxxTestではソースコードを直接ビルドするのではなく、C++のソースコードからテスト登録処理などを付加したC++ソースコードを生成し、それをビルドします。

以下はCxxTest User Guideにあるソースコードをベースにしています。テストの定義だけでテスト登録処理は含まれていません。

// MyTestSuite.h
#include <cxxtest/TestSuite.h>

class MyTestSuite : public CxxTest::TestSuite
{
public:
   void testAddition( void )
   {
      TS_ASSERT( 1 + 1 > 1 );
      TS_ASSERT_EQUALS( 1 + 1, 2 );
   }
};

以下のようにビルドします。

% cxxtestgen --error-printer -o cxxunit-tests.cpp MyTestSuite.h
% g++ -o cxxunit-tests cxxunit-tests.cpp

cxxtestgenMyTestSuite.hにあるテスト定義にテスト登録処理などを加えてcxxunit-tests.cppを生成します。余談ですが、cxxtestgenはPythonスクリプトです。また、CxxUnitはライブラリを提供せず、ヘッダーファイルのみを提供します。

バイナリを実行するとテストが走ります。

% ./cxxunit-tests
Running 1 test.OK!

テスト登録が完全に自動化されているのでCppUnitよりも新規テストの追加が容易です。テストの登録しわすれもありません。ただ、cxxtestgenとC++コンパイラで2回コンパイルする必要があることが少し手間だと言えます。

QTestLib

続いてQtが提供するQTestLibのケースです。QTestではQtが提供するスロットの仕組みを使って、定義されているテストを集めます。スロットがどのように定義されているかをプログラム中から扱うために、QtはC++のソースコードをプリコンパイルしますが、QTestでも同様にプリコンパイルする必要があります1

以下はQTestLibのチュートリアルにあるソースコードををベースにしています。

// test-qstring.cpp
#include <QtTest/QTest>

class TestQString: public QObject
{
    Q_OBJECT
private slots:
    void toUpper()
    {
        QString str = "Hello";
        QCOMPARE(str.toUpper(), QString("HELLO"));
    }
};

QTEST_MAIN(TestQString)
#include "test-qstring.moc"

QTestLibでもmain関数を定義しなければいけませんが、QTEST_MAINという便利マクロが用意されています。

以下のようにビルドします。

% mkdir test-qstring
% mv test-qstring.cpp test-qstring
% cd test-qstring
% qmake -project "QT += testlib"
% qmake
% make

バイナリを実行するとテストが走ります。

% ./test-qstring
********* Start testing of TestQString *********
Config: Using QTest library 4.5.3, Qt 4.5.3
PASS   : TestQString::initTestCase()
PASS   : TestQString::toUpper()
PASS   : TestQString::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of TestQString *********

このようにQTestLibではテスト登録のために必要なコードはQTEST_MAINでクラスを指定している部分だけです。個々のテストは指定する必要がありません。

メタオブジェクト情報を生成すること、また、それを読み込んでいる#include "test-qstring.moc"のところはQTestLib独自のことではなく、Qt全般のことです。そのため、Qtを利用している場合は追加で必要な作業とはいえないでしょう。つまり、QTestLibのテストを集める方法は完全には自動化されていませんが、Qt開発者にはそれほど負担もかからず自然に書けるようになっている使いやすいAPIといえます。一方、Qt開発者でない場合は、面倒に見えるでしょう。

マクロでテスト定義と同時に登録する

CppUnitでもマクロでテストを登録していますが、それをもう一歩進めたのがこの方法です。CppUnitでは、テスト定義は通常の関数定義でしたが、この方法ではそこでマクロを使い、テスト定義と同時にテストを登録します。

Google Test

まずは、Google Testです。Google Testではテスト定義時にTESTマクロを使います。以下はGoogleTestSamplesにあるソースコードをベースにしています。

// test-factorial.cpp
#include <gtest/gtest.h>

int Factorial(int n) {
  int result = 1;
  for (int i = 1; i <= n; i++) {
    result *= i;
  }

  return result;
}

TEST(FactorialTest, Negative) {
  EXPECT_EQ(1, Factorial(-5));
  EXPECT_EQ(1, Factorial(-1));
  EXPECT_TRUE(Factorial(-10) > 0);
}

int main(int argc, char **argv) {
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

プリコンパイル方式でもテストの登録忘れはありませんが、この方法でも登録を忘れることがありません。テスト定義の方法が通常の関数定義とは異なる書式になることに慣れることができるのであれば、この方式で負担が少なくテストを書けるようになるでしょう。

以下のようにビルドします。

% g++ -o test-factorial test-factorial.cpp -lgtest

バイナリを実行するとテストが走ります。

% ./test-factorial
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from FactorialTest
[ RUN      ] FactorialTest.1
[       OK ] FactorialTest.1 (0 ms)
[----------] 1 test from FactorialTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[  PASSED  ] 1 test.

上記の例では触れていませんが、フィクスチャ(setup/teardown)を使う場合は、テストクラス名を揃える必要があるなど、同じグループのテストを作る場合は重複する部分がでてしまいます。例えば、QTestLibのようにクラス内にテストを定義する方法では以下のようになります。

class MyTest
{
   void setup() {...}
   void teardown() {...}

   void test1() {...}
   void test2() {...}
   void test3() {...}
}

一方、Google Testの場合は、スコープが使えず、以下のようにクラス名を複数回書く必要があります。

class FooTest : public testing::Test {
 protected:
  virtual void SetUp() { b_.AddElement(3); }

  Foo a_;
  Foo b_;
};

TEST_F(FooTest, InitializesCorrectly) {
  EXPECT_TRUE(a_.StatusIsOK());
}

TEST_F(FooTest, ReturnsElementCountCorrectly) {
  EXPECT_EQ(0, a_.size());
  EXPECT_EQ(1, b_.size());
}

マクロを使っている場合は、間違ったテストクラス名を指定するなどコンパイルエラーになったときに意味の分からないエラーメッセージを目にすることがあるというのも注意しなければいけないポイントです。エラーメッセージを使えないと問題を発見することが難しくなります。

Boost Test Library

次に、Boost Test Libraryです。やり方はGoogle Testとだいたい同じで、Boost Test LibraryではBOOST_AUTO_TEST_CASEを使います。

// test-add.cpp
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE AddTest
#include <boost/test/unit_test.hpp>

int add( int i, int j ) { return i+j; }

BOOST_AUTO_TEST_CASE( add_test )
{
    BOOST_CHECK_EQUAL( add( 2,3 ), 5);
}

最初にBOOST_TEST_DYN_LINKBOOST_TEST_MODULEを定義しておくと、Boost Test Libraryではmain関数を定義する必要はありません。

以下のようにビルドします。

% g++ -o test-add test-add.cpp -lboost_unit_test_framework

バイナリを実行するとテストが走ります。

% ./test-add
Running 1 test case...

*** No errors detected

Google Testと同じくマクロが気にならない場合やBoostに慣れている場合はテストが書きやすいでしょう。

共有ライブラリから探す

マクロを利用する方法は言語の構文を工夫してテストを集めています。プリコンパイル方式では言語の構文はそのままで、コンパイル前に付加情報を加えることでテストを集めています。

一方、最後の共有ライブラリから探す方法ではコンパイル後の共有ライブラリから情報を取得してテストを集めます。この方式では、テストを共有ライブラリとして作成し、テスト実行用のコマンドからその共有ライブラリを読み込み、テストを実行します。こうすることにより、テスト側にテスト登録処理を埋め込む必要がなくなります2。共有ライブラリの中からテストを見つける処理はテスト実行コマンドが頑張るからです。

WinUnit

最初はWinUnitです。やり方は共有ライブラリからテストを集める方式なのですが、書き方はマクロを使う方式です。テストを定義するときはBEGIN_TESTEND_TESTで囲みます。

#include "WinUnit.h"

BEGIN_TEST(AddTest)
{
   WIN_ASSERT_TRUE(3 == add(1, 2));
}
END_TEST

すでにGoogle TestやBoost Test Libraryで見たように、この使い方であれば、共有ライブラリにする必要はありません。マクロの中で一工夫することでテストの自動登録を実現できるからです。

WinUnitの利点はVisual C++で使いやすいことでしょう。マクロを使ったAPIが気にならないVisual C++開発者には有力な選択肢です。

Cutter

最後はCutterです。CutterはC言語用の単体テストフレームワークとして開発されていましたが、先日リリースされた1.1.0で大きくC++対応を強化しています。

CutterではWinUnitとは違いマクロを利用しません。通常通り関数を定義するとテストとして認識されます。ただし、すべての関数がテストとして認識されるのではなく、test_からはじまる名前の関数だけをテストとして認識します。

#include <cppcutter.h>

namespace calc
{
   void test_add()
   {
       cppcut_assert_equal(5, add(2, 3));
   }
}

マクロを利用してテストを自動登録する方式では、フィクスチャ定義時に名前を揃える必要がありましたが、Cutterでは以下のようにnamespace内にsetup()/teardown()を定義するだけです。namespaceでグループ化されたテスト全体でフィクスチャを共有します。

#include <cppcutter.h>

namespace calc
{
   void setup() {...}
   void teardown() {...}

   void test_add() // calc::setup()/calc::teardown()が呼び出される
   {
       cppcut_assert_equal(5, add(2, 3));
   }

   void test_sub() // calc::setup()/calc::teardown()が呼び出される
   {
       cppcut_assert_equal(5, add(8, 3));
   }
}

この方式では通常のC++プログラムと同様にテストを書くことができるため、新しくテストを書くことの敷居が低くなります。しかし、tes_などタイプミスをしてしまった場合に、どうしてテストが実行されないのかに気づきにくいという問題点があります。

テストも通常のプログラムと同様に開発したい場合はマクロを使わないこの方式がオススメです。

まとめ

C++用の各種xUnitでのテストの書き方を、方式毎に分類して紹介しました。どんなバックグラウンドを持っているかにより、選びやすいxUnitは変わるでしょう。Visual C++開発者であればWinUnitを選ぶことが多いでしょうし、Qt開発者であればQtTestLibを選ぶことが多いでしょう。しかし、バックグラウンドから選ぶだけではなく、テストの書きやすさも判断材料に加えてみてはいかがでしょうか。

継続して開発すればそれに伴ってテストも増えていきます。しかし、テストは面倒くさがって飛ばしてしまいがちです。新しくテストを書く敷居が下がれば、テストを面倒くさがることが少なくなり、安心して開発を続けていくための土台を固めることができます。新しくテストを書く敷居を下げることは開発を継続するのであれば割に合うということです。

今回は「新しいテストの書きやすさ」を軸に様々なxUnitのやり方を紹介しました。C++用xUnitを選択する時の参考にしてみてください。

念のため書きますが、オススメはCutterです。

  1. プリコンパイルにはmoc(メタオブジェクトコンパイラ)を使います。

  2. 「これがテストだよ」という目印は埋め込む必要があります。