[ C++で開発 ]
CppUnitを使ってユニットテストを行います。ここでいうユニットテストとは、関数やクラスのメンバ関数を対象とする単体テストのことで、テストケースとして所定の入力を与えて関数(メンバ関数)を実行し、実行結果である出力を所定の入力に対する期待値と比較照合して良否を判断するものです。
CppUnitを使うための環境構築を行います。CppUnitは、ソースコードで配布されているので、標準C++規格に適合するコンパイラであればおそらく環境が構築できると思います。
ここでは、Solaris OS上のSunStudio C++コンパイラ、Windows OS上のVisual C++コンパイラ、Linux(CentOS)上のGNU C++コンパイラ環境における構築を紹介しています。(各バージョンは本文参照)
次のサイトからソース一式(tarball)をダウンロードします。ソース一式はプラットフォームによらず共通です。
2008.4.19現在、1.12.1 が最新版です。
ビルドはプラットフォームによって異なります。
ソースから、configure/make/make install でインストールします。
work$ tar xzf cppunit-1.10.2.tar.gz work$ cd cppunit-1.10.2 cppunit-1.10.2$ ./configure CC=cc CXX=CC CXXFLAGS="-pta -instances=static -mt -xtarget=pentium3 -g -features=no%transitions -xildoff" LD=CC LDFLAGS=-xildoff : cppunit-1.10.2$ make : cppunit-1.10.2$ su cppunit-1.10.2# make install : cppunit-1.10.2$ exit
インストールされると、デフォルトでは/usr/local以下に展開されます。
ヘッダーファイル群 | /usr/local/include/cppunit | |
ライブラリファイル群 | /usr/local/lib直下 |
cppunit-1.12.0のINSTALL-unixに記載されているSolaris用のビルド記述は少し古いので、そのままではビルド時にエラーが発生します。
work$ tar xzf cppunit-1.12.0.tar.gz work$ cd cppunit-1.12.0 cppunit-1.12.0$ ./configure CC=cc CXX=CC CXXFLAGS="-pta -instances=static -mt -xtarget=generic -g -features=no%transitions -xildoff" LD=CC LDFLAGS=-xildoff : cppunit-1.12.0$ make : CC -G -zdefs -nolib -hlibcppunit-1.12.so.0 -o .libs/libcppunit-1.12.so.0.0.0 .libs/AdditionalMessage.o .libs/Assserter.o .libs/BeOsDynamicLibraryManager.o .libs/BriefTestProgressListener.o ... .libs/Win32DynamicLibraryManager.o -mt 未定義の 最初に参照している シンボル ファイル std::ostream &std::ostream::operator<<(int) .libs/CompilerOutputer.o std::ostream &std::ostream::operator<<(double) .libs/StringTools.o fabs .libs/TestAssert.o void __Crun::pure_error() .libs/CompilerOutputer.o : cppunit-1.12.0$
原因は、-zdefsオプションが指定されている、すなわち動的リンクライブラリを作成時に未定義シンボルがあるとエラーとするようにしていながら、-nolibオプションが指定され、標準では自動的にリンクされているlibCstd、libCrun、libc、libmがリンクされないので、未定義シンボルでエラーが出まくっています。
そこで、configure時に少々オプション定義を変更します。ついでにインストール先ディレクトリも変更します。/usr/localの下ではなく、/opt/cppunitとし、また、SunStudio C++コンパイラ用とGCC C++コンパイラ用と2種類のバイナリに対応できるよう、libディレクトリの下にsc、gcのサブディレクトリを用意します。
cppunit-1.12.0$ ./configure -prefix=/opt/cppunit -libdir=/opt/cppunit/lib/sc \ CC=cc CXX=CC CXXFLAGS="-pta -instances=global -mt -xtarget=generic -g \ -features=no%transitions" LD=CC LDFLAGS="-lCstd -lCrun -lc -lm" : cppunit-1.12.0$ gmake : cppunit-1.12.0$ gmake install : cppunit-1.12.0$
ソースから、VC++6用のワークスペースファイルCppUnitLibraries.dswを読み込み変換してからバッチビルドを行います。
以下のプロジェクトファイルがあります。
プロジェクト名 | 構成 | 生成物 | 備考 |
---|---|---|---|
cppunit |
Debug |
cppunitd.lib |
|
Release |
cppunit.lib |
||
cppunit_dll |
Debug |
cppunitd_dll.dll |
|
Release |
cppunit_dll.dll |
||
DllPlugInTester |
Debug |
DllPlugInTesterd_dll.exe |
|
Debug Static |
DllPlugInTesterd.exe |
||
Debug Unicode |
DllPlugInTesterud.exe |
||
Release |
DllPlugInTester_dll.exe |
||
Release Static |
DllPlugInTester.exe |
||
Release Unicode |
DllPlugInTesteru.exe |
||
DSPlugIn |
Debug Debug Unicode Release Release Unicode |
VC++6専用 | |
TestPlugInRunner |
Debug |
TestPlugInRunnerd.exe |
|
Release |
TestPlugInRunner.exe |
||
TestRunner |
Debug |
testrunnerd.dll |
|
Debug Unicde |
testrunnerud.dll |
||
Release |
TestRunner.dll |
||
Release Unicode |
testrunneru.dll |
バッチビルドが完了すると、これら生成物はcppunit-1.12.0\libディレクトリ化にコピーされています。そこで、cppunit-1.12.0の下のincludeおよびlibを然るべきところへコピーします。
D:\temp\cppunit-1.12.0> mkdir D:\lib\cppunit D:\temp\cppunit-1.12.0> mkdir D:\lib\cppunit\include D:\temp\cppunit-1.12.0> mkdir D:\lib\cppunit\lib D:\temp\cppunit-1.12.0> xcopy include D:\lib\cppunit\include /S /E D:\temp\cppunit-1.12.0> xcopy lib D:\lib\cppunit\lib /S /E D:\temp\cppunit-1.12.0>
tarballにcppunit.specファイルが含まれているので、これをベースに修正を行ってRPMパッケージを作成します。
ビルド手順の詳細は、CentOS 5バイナリ・パッケージの記事を参照してください。
インストール先は、システム・インクルード・ディレクトリ(/usr/include)およびシステム・ライブラリ・ディレクトリ(/usr/lib)になるので、ビルド時のオプション指定は最小限で済みます。
work$ CC -I/usr/local/include -c SimpleTest.cc :
work$ CC -L/usr/local/lib -o SimpleTest SimpleTest.o -lcppunit work$ ./SimpleTest ld.so.1: ./SimpleTest: 重大なエラー: libcppunit-1.10.2.so.2: open に失敗しました: ファイルもディレクトリもありません。 強制終了 work$ export LD_LIBRARY_PATH=/usr/local/lib work$ ./SimpleTest : work$
work$ CC -L/usr/local/lib -o SimpleTest SimpleTest.o -Bstatic -lcppunit :
詳細は、リンク・情報源を参照ください。ここでは、各種情報源ではカバーできない最新版の情報や、はまり所をメモします。
慣習で、テスト対象クラス名+Testと名前を付けますが、分かれば別な名前でも構いません。テストクラスは、CppUnitのTestFixtureクラスを継承して作成するのが一般的です。
C++ではヘッダーファイルとソースファイルの両方をメンテナンスしなければならないので、頻繁にメンバ関数を追加していくテスト駆動開発でのテストクラスはソースファイルだけに記述するのがよいと思います。それに、ヘッダーファイルを設けても誰も使用しないですし。
// MyComponentTest.cpp #include <cppunit/extensions/HelperMacros.h> class MyComponentTest : public CPPUNIT_NS::TestFixture { CPPUNIT_TEST_SUITE(MyComponentTest); // この間にテスト実行メンバ関数を指定するマクロを記述(複数) CPPUNIT_TEST_SUTIE_END(); public: virtual void setUp() { } virtual void tearDown() { } }; CPPUNIT_TEST_SUITE_REGISTRATION(MyComponentTest);
上記がテストクラスの最小限の雛形です。
CPPUNIT_NSはCppUnitの名前空間のマクロでおそらくnamespace未対応コンパイラ用にマクロ化されたもの?
テストクラスにテスト・メンバ関数を定義するときは、CPPUNIT_TESTマクロも定義します。
: class MyComponentTest : public CPPUNIT_NS::TestFixture { CPPUNIT_TEST_SUITE(MyComponentTest); CPPUNIT_TEST(testPositiveUpperCase); CPPUNIT_TEST_SUTIE_END(); public: : void testPositiveUpperCase() { MyComponent compo; compo.set(INT_MAX); CPPUNIT_ASSERT_EQUAL(compo.get(), INT_MAX); } }; :
メンバ関数では、テストを実行し、結果をCPPUNIT_ASSERT_EQUALマクロで期待値と比較します。比較結果がテスト結果として記録されます。
テストクラスを実行し結果を出力するメインプログラムを定義します。
以下は、@ITの記事「C++アプリケーションの効率的なテスト手法(CppUnit編)」(えぴすてーめー氏)で紹介しているテスト・ランナー(CppUnitテストケース実行用メイン関数)です。
// Main.cpp #include <cppunit/BriefTestProgressListener.h> #include <cppunit/CompilerOutputter.h> #include <cppunit/extensions/TestFactoryRegistry.h> #include <cppunit/TestResult.h> #include <cppunit/TestResultCollector.h> #include <cppunit/TestRunner.h> int main(int argc, char* argv[]) { CPPUNIT_NS::TestResult controller; CPPUNIT_NS::TestResultCollector result; controller.addListener(&result); CPPUNIT_NS::BriefTestProgressListener progress; controller.addListener(&progress); CPPUNIT_NS::TestRunner runner; runner.addTest(CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()); runner.run(controller); CPPUNIT_NS::CompilerOutputter outputter(&result, CPPUNIT_NS::stdCOut()); outputter.write(); return result.wasSuccessful() ? 0 : 1; }
テストクラスでCPPUNIT_TEST_SUITE_REGISTRATIONマクロにより登録されたテストケースを実行する仕組みになっており、メイン関数から直接テストケースを呼び出す部分はありません。
ただし、テスト・ランナーのコンパイル・リンク時に、テストクラスが含まれるライブラリをリンクする必要があります。
テスト・ランナーのコードはテストクラスが変わっても修正不要ですが、リンクをし直す必要があります。そこで、実行時に文字列で指定されたテストクラスのライブラリファイルをロードするように修正し、テストクラスのライブラリを自由に指定できるようにしてみました。コードはLinux用ですが、ライブラリの動的ロードはWindowsでも可能です。
以下に、上記テスト・ランナーのコード(Main.cpp)に追加する部分だけ抜粋して示します。
#include <dlfcn.h> int loadLibrary(const char* filename) { void* handle = dlopen(filename, RTLD_NOW|RTLD_GLOBAL); if (handle == 0) { std::cerr << "[Error]dlopen failed for " << filename << std::endl; return -1; } return 0; } int main(int argc, char* argv[]) { if (argc <= 1) { std::cerr << "no test case library specified in argument" << std::endl; return -1; } for (int i=1; i<argc; i++) { loadLibrary(argv[i]); } :(略)
テストを実行し結果を出力するメイン・プログラムをテスト・ランナーと呼びます。CppUnitでは、テスト・ランナーもアプリケーション開発者が記述する必要があります。
CppUnit標準で提供されるテキストUIに加え、いくつかのGUIライブラリ用テスト・ランナーも公式・非公式に提供されています。MFC, Qt 2.x, Qt 4.x, Curses, WxWindows など。
QxRunnerと呼ばれるQt 4.x用のテスト・ランナー構築用ライブラリがあります。
2008年5月5日現在の最新版は0.9.2です。ソースアーカイブqxrunner.0.9.2.tar.gzをダウンロードします。
2つのライブラリ qxcppunit と qxrunner をビルドします。付属のREADME.txtには、VisualStudio2005およびLinux用のビルド手順が記載されています。以下Linux(CentOS 5、Qt 4.3.4、GCC4.1.2)における手順を示します。
ビルドするライブラリ種類として静的リンクライブラリ、動的リンクライブラリを指定できます。また、デバッグビルド、リリースビルドを指定します。ユニットテスト環境なので、ここでは動的リンクライブラリのデバッグビルド版をビルドします。
work$ tar xvzf qxrunner.0.9.2.tar.gz : work$ cd qxrunner qxrunner$ cd src/qxrunner qxrunner$ qmake qxrunner.pro "CONFIG+=debug_dll" : qxrunner$ make : qxrunner$ cd ../qxcppunit qxcppunit$ qmake qxcppunit.pro "CONFIG+=debug_dll" : qxcppunit$ make : qxcppunit$ cd ../../lib lib$ ls libqxcppunitd_shared.so libqxrunnerd_shared.so libqxcppunitd_shared.so.1 libqxrunnerd_shared.so.1 libqxcppunitd_shared.so.1.0 libqxrunnerd_shared.so.1.0 libqxcppunitd_shared.so.1.0.0 libqxrunnerd_shared.so.1.0.0 lib$
インストール先は、RPMパッケージ化していないので、/usr/localの下とします。
# cd work # mv qxrunner /usr/local/ #
// QtMain.cpp #include <QApplication> #include <qxcppunit/testrunner.h> #include <cppunit/extensions/TestFactoryRegistry.h> int main(int argc, char* argv[]) { QApplication app(argc, argv); QxCppUnit::TestRunner runner; runner.addTest( CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()); runner.run(); return 0; }