今回はC/C++のユニットテストフレームワークのひとつである CppUTest について調べてみました。TDDと相性が良いと言われている Google Test と比較して、CppUTest はマルチプラットフォーム対応がより充実しているためWeb系に限らず組み込み系などでも幅広く使われているとのことです。
参考: テスト駆動開発による組み込みプログラミング ―C言語とオブジェクト指向で学ぶアジャイルな設計
これまで不満はありつつも使い慣れているからという理由で CppUnit をずっと使っていたのですが、最近は業務でC++ライブラリを作る機会が増えてきたこともあり、より効率良く仕事を進めるために新しいツールを使ってみようという気になりました。
環境
CentOS 6.4 (x86_64)
GCC 4.4.7
CppUTest 3.4
インストール
Debian系なら apt-get install cpputest
でOKなのですが、Redhat系だとrpmパッケージはない(? みたいなので、ここではソースコードをコンパイルしてインストールします。最新版のCppUTestはCMake対応しているので環境に合わせて簡単に導入することができます。もし環境にCMakeが入っていなければyumで2.8系を入れておきます。
CMakeのインストール
1 2 |
## CMakeが入っていなければインストールしておく $ sudo yum cmake28 |
CppUTestのインストール
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
## ソースコードの取得 $ git clone git://github.com/cpputest/cpputest.git $ cd cpputest $ mkdir workspace && cd workspace ## cmakeを実行 ## お試しなのでユーザーディレクトリ以下にインストール、カバレッジ(gcov)をサポート $ cmake28 -DCMAKE_INSTALL_PREFIX=./install -DCOVERAGE=ON .. ------------------------------------------------------- CppUTest Version 3.4 Current compiler options: CC: /usr/bin/cc CXX: /usr/bin/c++ ... 省略 Features configure in CppUTest: Memory Leak Detection: ON Compiling Extensions: ON Use CppUTest flags: ON Using Standard C library: ON Using Standard C++ library: ON Using C++11 library: OFF Generating map file: OFF Compiling with coverage: ON ------------------------------------------------------- ## コンパイル $ make -j4 ## インストール (ユーザーディレクトリにインストールするので sudo は不要) $ make install ## ヘッダファイルとスタティックライブラリがインストールされる $ ls install/*/* install/lib/libCppUTest.a install/lib/libCppUTestExt.a install/include/CppUTest: CommandLineArguments.h JUnitTestOutput.h MemoryLeakDetectorNewMacros.h PlatformSpecificFunctions_c.h TestFailure.h TestHarness_c.h TestPlugin.h TestTestingFixture.h CommandLineTestRunner.h MemoryLeakDetector.h MemoryLeakWarningPlugin.h SimpleString.h TestFilter.h TestMemoryAllocator.h TestRegistry.h Utest.h CppUTestConfig.h MemoryLeakDetectorMallocMacros.h PlatformSpecificFunctions.h StandardCLibrary.h TestHarness.h TestOutput.h TestResult.h UtestMacros.h install/include/CppUTestExt: CodeMemoryReportFormatter.h GTest.h MemoryReportAllocator.h MemoryReporterPlugin.h MockExpectedFunctionCall.h MockFailure.h MockNamedValue.h MockSupportPlugin.h OrderedTest.h GMock.h GTestConvertor.h MemoryReportFormatter.h MockActualFunctionCall.h MockExpectedFunctionsList.h MockFunctionCall.h MockSupport.h MockSupport_c.h |
GCC 4.4系だとC++11サポートフラグは効かないようなので外しています。まぁ実務でC++11を使わせてもらえるような環境ってまだあまりないと思いますが。
glibc-staticのインストール
CentOS 6.x系だとGCCでスタティックリンクするためには glibc-static を別途インストールする必要があるので注意してください。5.x系では不要です。
1 2 |
## yumで入るパッケージでOK $ sudo yum install glibc-static |
Getting Started
簡単なアフィン変換行列のクラスを作ってテストを書いてみます。
* geom.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#ifndef GEOM_H #define GEOM_H #include <cmath> #include <sstream> /** * affine transform matrix * | a c tx | * | b d ty | * | 0 0 1 | **/ namespace geom { class AffineMatrix { public: float a, b, c, d, tx, ty; AffineMatrix(); AffineMatrix(float a, float b, float c, float d, float tx, float ty); virtual ~AffineMatrix(); AffineMatrix clone() const; void concat(const AffineMatrix& mat); void translate(float dx, float dy); void scale(float sx, float sy); void rotate(float degree); void skew(float kx, float ky); void identity(); }; } std::ostream& operator<<(std::ostream& os, const geom::AffineMatrix& mat); #endif |
* geom.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
#include "geom.h" namespace geom { AffineMatrix::AffineMatrix() : a(1.0), b(0.0), c(0.0), d(1.0), tx(0.0), ty(0.0) { } AffineMatrix::AffineMatrix(float a, float b, float c, float d, float tx, float ty) : a(a), b(b), c(c), d(d), tx(tx), ty(ty) { } AffineMatrix::~AffineMatrix() { } AffineMatrix AffineMatrix::clone() const { return AffineMatrix(a, b, c, d, tx, ty); } void AffineMatrix::concat(const AffineMatrix& mat) { a = a*mat.a + c*mat.b; b = b*mat.a + d*mat.b; c = a*mat.c + c*mat.d; d = d*mat.c + d*mat.d; tx = a*mat.tx + c*mat.ty + tx; ty = b*mat.tx + d*mat.ty + ty; } void AffineMatrix::translate(float dx, float dy) { tx = dx; ty = dy; } void AffineMatrix::scale(float sx, float sy) { a = sx; b = sy; } void AffineMatrix::rotate(float degree) { float radian = M_PI/180*degree; a = cos(radian); b = sin(radian); c = -sin(radian); d = cos(radian); } void AffineMatrix::skew(float kx, float ky) { b = tan(M_PI/180*ky); c = tan(M_PI/180*kx); } void AffineMatrix::identity() { a = 1.0; b = 0.0; c = 0.0; d = 1.0; tx = 0.0; ty = 0.0; } } std::ostream& operator<<(std::ostream& os, const geom::AffineMatrix& mat) { os << std::endl; os << "[" << mat.a << ", " << mat.c << ", " << mat.tx << "]" << std::endl; os << "[" << mat.b << ", " << mat.d << ", " << mat.ty << "]" << std::endl; os << "[0, 0, 1]" << std::endl; return os; } |
* hello_cpputest.cpp
CppUTestを使ったユニットテストコードは以下のような感じになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
#include <iostream> #include "CppUTest/CommandLineTestRunner.h" #include "geom.h" // テストグループの定義 TEST_GROUP(group) // フィクスチャの準備 TEST_GROUP(MatrixTestGroup) { geom::AffineMatrix mat; const static double eps = __FLT_EPSILON__; TEST_SETUP() { // 各テストケース実行前に呼ばれる、ここではなにもしない } TEST_TEARDOWN() { // 各テストケース実行後に呼ばれる、ここではなにもしない } }; // テストケース TEST(group, name) TEST(MatrixTestGroup, TestConstruct) { DOUBLES_EQUAL(mat.a, 1.0, eps); DOUBLES_EQUAL(mat.b, 0.0, eps); DOUBLES_EQUAL(mat.c, 0.0, eps); DOUBLES_EQUAL(mat.d, 1.0, eps); DOUBLES_EQUAL(mat.tx, 0.0, eps); DOUBLES_EQUAL(mat.ty, 0.0, eps); } TEST(MatrixTestGroup, TestClone) { geom::AffineMatrix clone_mat = mat.clone(); DOUBLES_EQUAL(clone_mat.a, mat.a, eps); DOUBLES_EQUAL(clone_mat.b, mat.b, eps); DOUBLES_EQUAL(clone_mat.c, mat.c, eps); DOUBLES_EQUAL(clone_mat.d, mat.d, eps); DOUBLES_EQUAL(clone_mat.tx, mat.tx, eps); DOUBLES_EQUAL(clone_mat.ty, mat.ty, eps); } TEST(MatrixTestGroup, TestConcat) { geom::AffineMatrix mat2; mat.concat(mat2); DOUBLES_EQUAL(mat.a, 1.0, eps); DOUBLES_EQUAL(mat.b, 0.0, eps); DOUBLES_EQUAL(mat.c, 0.0, eps); DOUBLES_EQUAL(mat.d, 1.0, eps); DOUBLES_EQUAL(mat.tx, 0.0, eps); DOUBLES_EQUAL(mat.ty, 0.0, eps); } TEST(MatrixTestGroup, TestTranslate) { mat.translate(10.0, 20.0); DOUBLES_EQUAL(mat.tx, 10.0, eps); DOUBLES_EQUAL(mat.ty, 20.0, eps); } TEST(MatrixTestGroup, TestScale) { mat.scale(2.0, 3.0); DOUBLES_EQUAL(mat.a, 2.0, eps); DOUBLES_EQUAL(mat.b, 3.0, eps); } TEST(MatrixTestGroup, TestRotate) { float radian = M_PI/180*M_PI; mat.rotate(M_PI); DOUBLES_EQUAL(mat.a, cos(radian), eps); DOUBLES_EQUAL(mat.b, sin(radian), eps); DOUBLES_EQUAL(mat.c, -sin(radian), eps); DOUBLES_EQUAL(mat.d, cos(radian), eps); } TEST(MatrixTestGroup, TestSkew) { mat.skew(10.0, 20.0); DOUBLES_EQUAL(mat.b, tan(M_PI/180*20.0), eps); DOUBLES_EQUAL(mat.c, tan(M_PI/180*10.0), eps); } TEST(MatrixTestGroup, TestIdentity) { mat.identity(); DOUBLES_EQUAL(mat.a, 1.0, eps); DOUBLES_EQUAL(mat.b, 0.0, eps); DOUBLES_EQUAL(mat.c, 0.0, eps); DOUBLES_EQUAL(mat.d, 1.0, eps); DOUBLES_EQUAL(mat.tx, 0.0, eps); DOUBLES_EQUAL(mat.ty, 0.0, eps); } TEST(MatrixTestGroup, TestPrintMatrix) { std::stringstream ss; std::string cmp = "\n[1, 0, 0]\n[0, 1, 0]\n[0, 0, 1]\n"; ss << mat; CHECK_EQUAL(ss.str(), cmp); } int main(int argc, char** argv) { // テストランナー return RUN_ALL_TESTS(argc, argv); } |
テストコードの全体的な流れとしてはGoogle Testとよく似ています。CppUTestは自動テストディスカバリ機能を持っているので、CppUnitのようにいちいちテストケースを登録する必要がないので記述が非常に簡潔になります。また、各マクロ名が短いのも地味に嬉しいです。Boost Testはマクロ名が長いので書くのが面倒ですよね。
* Makefile
CPPUTEST_HOME にはCppUTestのインストールディレクトリを指定します(ここではユーザーディレクトリ以下にインストールしてある)。また、gcov関連のオプションも忘れずに指定しておきます。ここでは手でMakefileを書いていますが、CppUTestのポリシー的にマルチプラットフォーム対応を考えてCMakeなどでMakefileを生成するのが良いんでしょうね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
CXX = g++ CXXFLAGS = -g -Wall -static -fprofile-arcs -ftest-coverage -I./ -I$(CPPUTEST_HOME)/include LDFLAGS = -L./ -L$(CPPUTEST_HOME)/lib -lCppUTest -lCppUTestExt CPPUTEST_HOME = ./cpputest/workspace/install TARGET = hello_cpputest SRCS = hello_cpputest.cpp geom.cpp OBJS = $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) $(CXX) -o $@ $^ $(CXXFLAGS) $(LDFLAGS) $(OBJS): $(SRCS) $(CXX) -c $(CXXFLAGS) $^ %.o: %.cpp $(CXX) -c $(CXXFLAGS) $< .PHONY: clean clean: rm -f $(TARGET) $(OBJS) *.gcno *.gcov *~ find . -name "*.gcda" | xargs -r rm |
実行結果は以下のようになります。実行時オプションもいくつか紹介。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
$ ./hello_cpputest ......... OK (9 tests, 9 ran, 34 checks, 0 ignored, 0 filtered out, 1 ms) ## -v オプションでテスト項目も表示できる $ ./hello_cpputest -v TEST(MatrixTestGroup, TestPrintMatrix) - 1 ms TEST(MatrixTestGroup, TestIdentity) - 0 ms TEST(MatrixTestGroup, TestSkew) - 0 ms TEST(MatrixTestGroup, TestRotate) - 0 ms TEST(MatrixTestGroup, TestScale) - 0 ms TEST(MatrixTestGroup, TestTranslate) - 0 ms TEST(MatrixTestGroup, TestConcat) - 0 ms TEST(MatrixTestGroup, TestClone) - 0 ms TEST(MatrixTestGroup, TestConstruct) - 0 ms OK (9 tests, 9 ran, 34 checks, 0 ignored, 0 filtered out, 1 ms) ## -r# オプションで指定回数繰り返してテストを実行できる $ ./hello_cpputest -r3 Test run 1 of 3 ......... OK (9 tests, 9 ran, 34 checks, 0 ignored, 0 filtered out, 1 ms) Test run 2 of 3 ......... OK (9 tests, 9 ran, 34 checks, 0 ignored, 0 filtered out, 0 ms) Test run 3 of 3 ......... OK (9 tests, 9 ran, 34 checks, 0 ignored, 0 filtered out, 0 ms) ## -g オプションでテストグループを指定して実行できる ## (ただし今回はグループを1つしか作ってないので結果は同じ) $ ./hello_cpputest -g MatrixTestGroup ......... OK (9 tests, 9 ran, 34 checks, 0 ignored, 0 filtered out, 0 ms) ## -n オプションで1つのテストを指定して実行できる $ ./hello_cpputest -n TestConstruct . OK (9 tests, 1 ran, 6 checks, 0 ignored, 8 filtered out, 0 ms) ## -ojunit オプションでJUnit形式(XML)でテスト結果を書き出せる $ /hello_cpputest -ojunit $ cat cpputest_MatrixTestGroup.xml cat cpputest_MatrixTestGroup.xml [/home/ryo/workspace/dev/junk/unittest] <?xml version="1.0" encoding="UTF-8" ?> <testsuite errors="0" failures="0" hostname="localhost" name="MatrixTestGroup" tests="9" time="0.000" timestamp="2014-03-01T17:53:25"> <properties> </properties> <testcase classname="MatrixTestGroup" name="TestPrintMatrix" time="0.000"> </testcase> <testcase classname="MatrixTestGroup" name="TestIdentity" time="0.000"> </testcase> ... 省略 |
JUnit形式で結果を書き出してくれるのでCIツールとも相性が良いです。ついでにカバレッジ計測も。
1 2 3 4 5 |
$ gcov geom.gcda | grep geom -B1 File 'geom.cpp' Lines executed:100.00% of 48 geom.cpp:creating 'geom.cpp.gcov' |
アサーションマクロ
公式マニュアルには以下のマクロが定義されていると書かれていますが、CppUTestのソースコードを見てると他にもたくさんのマクロが定義されていました。詳細は CppUTest/UtestMacros.h を参照してください。例外のチェック用マクロなどもあるようです。
1 2 3 4 5 6 7 8 9 |
CHECK(boolean condition) CHECK_TEXT(boolean condition, text) CHECK_EQUAL(expected, actual) STRCMP_EQUAL(expected, actual) LONGS_EQUAL(expected, actual) BYTES_EQUAL(expected, actual) POINTERS_EQUAL(expected, actual) DOUBLES_EQUAL(expected, actual, tolerance) FAIL(text) |
テスト対象のクラスが operator==()
をオーバーロードしていると、CHECK_EQUALでインスタンス同士の等値比較もできます。
メモリリークの検出
CppUTestにはデフォルトでメモリリーク検出機能も付いています。普通にテストコードを走らせるだけでテストケース単位で検知してくれるようです。ためしにわざとリークするコードを書いて試してみます。
1 2 3 4 5 |
## geom::AffineMatrix#clone 内で適当にnewを入れておく AffineMatrix AffineMatrix::clone() const { float* f = new float(); return AffineMatrix(a, b, c, d, tx, ty); } |
* テスト実行
1 2 3 4 5 6 7 8 9 |
....... hello_cpputest.cpp:26: error: Failure in TEST(MatrixTestGroup, TestClone) Memory leak(s) found. Alloc num (13) Leak size: 4 Allocated at: <unknown> and line: 0. Type: "new" Memory: <0x239bec0> Content: "" Total number of leaks: 1 .. Errors (1 failures, 9 tests, 9 ran, 34 checks, 0 ignored, 0 filtered out, 1 ms) |
setup/teardown 実行時のメモリ使用量の差分でリークを検出する実装になっていました。CppUTest側で operator new/delete
をオーバーロードしているので、STLやBoostなどを使うとオーバーロード定義がコンフリクトしてしまうことがありますが、ヘッダ読み込みの順番を制御して回避できます。
ユーティリティツール
CppUTestにはシェルスクリプトで書かれた便利なツール群が付属しています。ここではその中の1つを紹介します。
* NewClass.sh
C++クラス用の .h/.cpp ファイルと対応するテストクラスファイルを生成してくれます。生成されるコードのスタイル等はテンプレートを定義することができます。各テンプレートファイルは $(CPPUTEST_HOME)/scripts/templates 以下にあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
## Fooクラス用のファイルを生成する $ ./NewClass.sh Foo creating Foo.h creating Foo.cpp creating FooTest.cpp $ cat Foo.h #ifndef D_Foo_H #define D_Foo_H /////////////////////////////////////////////////////////////////////////////// // // Foo is responsible for ... // /////////////////////////////////////////////////////////////////////////////// class Foo { public: explicit Foo(); virtual ~Foo(); private: Foo(const Foo&); Foo& operator=(const Foo&); }; #endif // D_Foo_H $ cat Foo.cpp #include "Foo.h" Foo::Foo() { } Foo::~Foo() { } $ cat FooTest.cpp #include "Foo.h" //CppUTest includes should be after your and system includes #include "CppUTest/TestHarness.h" TEST_GROUP(Foo) { Foo* foo; void setup() { foo = new Foo(); } void teardown() { delete foo; } }; TEST(Foo, Create) { FAIL("Start here"); } |
こういうコードジェネレータ系のユーティリティツールって最近はRuby製のものが多いですけど、全てシェルスクリプトで書かれているのを見るとマルチプラットフォーム対応を強く意識しているのがわかりますね。
感想
今まで使っていたCppUnitを比較すると、面倒な手順を踏まなくても簡単にテストコードを追加できるし、自動テストディスカバリ機能のおかげでテストランナーの記述が非常に簡潔になるのでTDDと相性が良いと感じました。軽快で実行効率にも優れているので組み込み系で実績があるのも納得できます。各プラットフォーム依存の実装は $(CPPUTEST_HOME)/src/Platform 以下にあるのでそれぞれ読み比べてみるのも面白いです。
あと、今回は試していませんがCppUTestはモックもサポートしています。サンプルを見る限りけっこう抽象化されていて使いやすそうです。
これからはCppUTestを使ってTDDで効率よく開発していきたいと思います。