CppUnit(1.9.6)と子河童/小馬によるテストファーストデザイン
$Date: 2002/06/06 02:16:01 $
河童プロジェクト επιστημη
インタフェースを決めよう
さて、君は今、'カウンタ:Counter'を作ってくれと頼まれた。クライアントさんが言うには、そのカウンタは次の2つの要件を満たして欲しいとのこと:
- 初期値が0であること
- カウント値をインクリメント(+1)できること
腕のたつ君のことだ、もうヘッダを書き始めていることだろう。こんな感じでね:
Counter.h
#ifndef COUNTER_H
#define COUNTER_H
class Counter {
private:
int count_; // カウント値
public:
Counter(); // [1] 初期化
void incr(); // [2] インクリメント
int count() const; // カウント値を返す
};
#endif
|
上出来だね...で、これだけだとCounterの各メソッドは宣言されてはいるものの実装(定義)されていないから、コンパイルできるけどリンクに失敗するに決まってる。とりあえずリンクまではできるよう、中身カラッポのハリボテ実装を用意しておこう:
Counter.cpp
#include "Counter.h"
Counter::Counter() {
// 何もしない
}
void Counter::incr() {
// 何もしない
}
int Counter::count() const {
return -123; // テキトーな値
}
|
このハリボテ実装に手を加え、そしてテストしながらクライアントに呈示された要件を満足する'本物'に近づけていこうってわけだ。
テストの実装を始めよう
テストファーストデザインでは、まずはじめにテストを書く。そしてそのテストが成功するように実装を埋めるんだ。
子河童はテストコードの雛形を吐いてくれる小さなツールだ。C++コードをCppUnitでテストするとき、テストコードはCppUnitが定める'お作法'に従っていなければならない。子河童はそのお作法通りのテストコードを吐いてくれる。お作法は子河童にまかせ、君は与えられた要件を満たすことを検証するコードを書けばいいのさ。
早速子河童を使ってみよう。子河童が吐くのはテストコードだ。Counterをテストするんだからテストコードの名前は'CounterTest'としようか。そしてテストメソッドは[1]初期化と[2]インクリメントを検証するのだからそれぞれ'testInit', 'testIncr'でどうだろう。そしてテストコードはCounter.hを#includeしなきゃいけないね。Counter.h/cpp を置いたディレクトリで子河童にひと働きしてもらおう:
cocuppa --include Counter.h --skeleton CounterTest testInit testIncr
子河童は CounterTest.cpp を吐いてくれたはずだ:
CounterTest.cpp
//CUPPA:separate=false
//CUPPA:namespace=
//CUPPA:include=+
#include "Counter.h"
//CUPPA:include=-
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/TestAssert.h>
class CounterTest : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(CounterTest);
//CUPPA:suite=+
CPPUNIT_TEST(testInit);
CPPUNIT_TEST(testIncr);
//CUPPA:suite=-
CPPUNIT_TEST_SUITE_END();
private:
// your stuff...
public:
virtual void setUp() { /* initialize */ }
virtual void tearDown() { /* terminate */ }
//CUPPA:decl=+
void testInit() {
CPPUNIT_FAIL("no implementation");
}
void testIncr() {
CPPUNIT_FAIL("no implementation");
}
//CUPPA:decl=-
};
//CUPPA:impl=+
//CUPPA:impl=-
CPPUNIT_TEST_SUITE_REGISTRATION(CounterTest);
|
吐かれたコードのあちこちに //CUPPA:...
っていうワケのわからないコメントが埋め込んである。
こいつらを書き換えないでくれ。
※ コマンドライン-インタフェースはダサい? 僕はそうは思わないけどな...子河童をGUIで実装したのが'子狐:cofox'だ。マルチプラットフォームGUIライブラリであるFOXによるもので、こんな外観だ。気が向いたら使ってみてほしい。

それではコンパイル...ちょっと待って、main()がなくちゃ動かしようがない。子河童はmain()を作ることもできるんだ。
cocuppa --main run
main()が定義された run.cpp ができたかな?
run.cpp
#include <iostream>
#include <string>
#include <cppunit/ui/text/TestRunner.h>
#include <cppunit/XmlOutputter.h>
#include <cppunit/TextOutputter.h>
#include <cppunit/CompilerOutputter.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
int main(int argc, char* argv[]) {
int format = 0;
int target = 0;
std::string xsl;
std::string ns;
for ( int i = 1; i < argc; ++i ) {
std::string arg(argv[i]);
if ( arg == "--text" ) format = 0;
if ( arg == "--xml" ) format = 1;
if ( arg == "--compiler" ) format = 2;
if ( arg == "--cerr" ) target = 1;
if ( arg == "--xsl" ) xsl = argv[++i];
if ( arg == "--namespace" ) ns = argv[++i];
}
CppUnit::TextUi::TestRunner runner;
if ( ns.empty() )
runner.addTest(CppUnit::TestFactoryRegistry::getRegistry().makeTest());
else
runner.addTest(CppUnit::TestFactoryRegistry::getRegistry(ns).makeTest());
CppUnit::Outputter* outputter = 0;
std::ostream* stream = target ? &std::cerr : &std::cout;
switch ( format ) {
case 0 :
outputter = new CppUnit::TextOutputter(&runner.result(),*stream);
break;
case 1 :
outputter = new CppUnit::XmlOutputter(&runner.result(),*stream, "shift_jis");
static_cast<CppUnit::XmlOutputter*>(outputter)->setStyleSheet(xsl);
break;
case 2 :
outputter = new CppUnit::CompilerOutputter(&runner.result(),*stream); break;
}
runner.setOutputter(outputter);
return runner.run() ? 0 : 1;
}
|
気を取り直してVisual C++でコンパイルしてみよう。CppUnitのインストールディレクトリがd:\CppUnitだとすると:
cl -GX -MD -Id:\CppUnit\include run.cpp CounterTest.cpp Counter.cpp d:\CppUnit\lib\cppunit_dll.lib
これでめでたくrun.exeができたことと思う。実行しよう:
実行結果
.F.F
!!!FAILURES!!!
Test Results:
Run: 2 Failures: 2 Errors: 0
1) test: CounterTest.testInit (F) line: 27 CounterTest.cpp
"no implementation"
2) test: CounterTest.testIncr (F) line: 30 CounterTest.cpp
"no implementation"
|
..."no implementation"だってさ。
子河童が吐いたテストコードそのままでは、必ずこのメッセージを出力して'失敗'する。
これでいいんだ。だってこれでテストメソッドtestInitとtestIncrが実行されたことが確認できただろ?
それに君はまだ要件を満たすことを検証するコードを一行も書いちゃいない。
実行結果はそのことを君に教えてくれたんだ。
テストの準備が整ったシルシだよ。
※ JUnitのグラフィカルなテストを見てしまうとテキストによるテスト結果は見劣りがするかい? 僕はテキスト版も味があって好きなんだけど...JUnitみたいにカッコいい外観も用意したんだ。名前は'狐火:foxfire'。子河童の吐くメインルーチンと入れ替えるとこんな見てくれが現れる。
テストメソッドを実装しよう
じゃぁその検証コード(テストメソッド)とやらを書くとしよう。CounterTest.cppを次のように修正すればいい:
CounterTest.cpp (追加/修正個所をboldで示す)
//CUPPA:separate=false
//CUPPA:namespace=
//CUPPA:include=+
#include "Counter.h"
//CUPPA:include=-
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/TestAssert.h>
class CounterTest : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(CounterTest);
//CUPPA:suite=+
CPPUNIT_TEST(testInit);
CPPUNIT_TEST(testIncr);
//CUPPA:suite=-
CPPUNIT_TEST_SUITE_END();
private:
// your stuff...
Counter* c_;
public:
virtual void setUp() { c_ = new Counter; }
virtual void tearDown() { delete c_; }
//CUPPA:decl=+
void testInit() {
CPPUNIT_ASSERT_EQUAL(0, c_->count());
}
void testIncr() {
for ( int i = 1; i < 10; ++i ) {
c_->incr();
CPPUNIT_ASSERT_EQUAL(i, c_->count());
}
}
//CUPPA:decl=-
};
//CUPPA:impl=+
//CUPPA:impl=-
CPPUNIT_TEST_SUITE_REGISTRATION(CounterTest);
|
メソッドsetUp
/tesrDown
はそれぞれ各テストメソッドの実行直前/直後に呼び出される。
テストメソッドの初期化/後始末ってワケだ。
そしてテストメソッドの中に書かれたマクロCPPUNIT_ASSERT_EQUAL
は、
その引数に'期待する値'と'実際の値'を与えることで両者を比較し、
異なっていたらテスト'失敗'として次のテストメソッドに移る。コンパイル/実行しよう:
実行結果
.F.F
!!!FAILURES!!!
Test Results:
Run: 2 Failures: 2 Errors: 0
1) test: CounterTest.testInit (F) line: 28 CounterTest.cpp
expected: 0
but was: -123
2) test: CounterTest.testIncr (F) line: 33 CounterTest.cpp
expected: 1
but was: -123
|
案の定失敗したね。だってまだハリボテ実装なんだものね。
少しづつ、書いてはテストしよう
では初期化から片付けるとしよう:
Counter.cpp
#include "Counter.h"
Counter::Counter() : count_(0) {
}
void Counter::incr() {
// 何もしない
}
int Counter::count() const {
return count_;
}
|
コンパイル/実行しよう:
実行結果
..F
!!!FAILURES!!!
Test Results:
Run: 2 Failures: 1 Errors: 0
1) test: CounterTest.testIncr (F) line: 33 CounterTest.cpp
expected: 1
but was: 0
|
よしよし、失敗がひとつ減ったね。残るはincr()だ:
Counter.cpp
#include "Counter.h"
Counter::Counter() : count_(0) {
}
void Counter::incr() {
++count_;
}
int Counter::count() const {
return count_;
}
|
コンパイル/実行しよう:
よっしゃ、これでCounterの一丁上がりさ。
テストを追加しよう
やれやれ、クライアントが機能追加を要求してきたぞ。そんなの最初に言ってくれよ...
- カウント値を0クリアできること
- カウント値をデクリメント(-1)できること
ああ、わかったわかった。やればいいんだろやれば...
できたばかりのCounterにちょっと手を入れよう。でも実装はカラッポにしておく。
Counter.h
#ifndef COUNTER_H
#define COUNTER_H
class Counter {
private:
int count_; // カウント値
public:
Counter(); // [1] 初期化
void incr(); // [2] インクリメント
int count() const; // カウント値を返す
void clear(); // [3] 0クリア
void decr(); // [4] デクリメント
};
#endif
|
Counter.cpp
#include "Counter.h"
Counter::Counter() : count_(0) {
}
void Counter::incr() {
++count_;
}
int Counter::count() const {
return count_;
}
void Counter::clear() {
// 何もしない
}
void Counter::decr() {
// 何もしない
}
|
次にやることは? もちろん追加分のテストに決まってる。
テストメソッド testClear と testDecr を CounterTest.cpp に追加しなきゃならない
申し訳ないんだが、子河童は最初の一歩を踏み出すための雛型を吐いてはくれるけど、
その後新たなテストメソッドを追加することはできないんだ。
こんなときには子馬を使ってほしい。
子河童が吐いたテストコードにテストメソッドを追加するのが子馬のお仕事だ。
couma --skeleton CounterTest testClear testDecr
これでテストコード CounterTest.cpp にテストメソッド testClear, testDecr が加えられているはずだ。
子馬も子河童と同様、"no implementation"を出力して失敗するテストメソッドを吐く。
クリア/デクリメントを検証するコードに置き換えよう:
CounterTest.cpp
//CUPPA:separate=false
//CUPPA:namespace=
//CUPPA:include=+
#include "Counter.h"
//CUPPA:include=-
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/TestAssert.h>
class CounterTest : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(CounterTest);
//CUPPA:suite=+
CPPUNIT_TEST(testInit);
CPPUNIT_TEST(testIncr);
CPPUNIT_TEST(testClear);
CPPUNIT_TEST(testDecr);
//CUPPA:suite=-
CPPUNIT_TEST_SUITE_END();
private:
// your stuff...
Counter* c_;
public:
virtual void setUp() { c_ = new Counter; }
virtual void tearDown() { delete c_; }
//CUPPA:decl=+
void testInit() {
CPPUNIT_ASSERT_EQUAL(0, c_->count());
}
void testIncr() {
for ( int i = 1; i < 10; ++i ) {
c_->incr();
CPPUNIT_ASSERT_EQUAL(i, c_->count());
}
}
void testClear() {
c_->incr();
c_->clear();
CPPUNIT_ASSERT_EQUAL(0, c_->count());
}
void testDecr() {
for ( int i = 1; i < 10; ++i ) {
c_->decr();
CPPUNIT_ASSERT_EQUAL(-i, c_->count());
}
}
//CUPPA:decl=-
};
//CUPPA:impl=+
//CUPPA:impl=-
CPPUNIT_TEST_SUITE_REGISTRATION(CounterTest);
|
コンパイル実行するとこんな結果が得られるだろう:
実行結果
...F.F
!!!FAILURES!!!
Test Results:
Run: 4 Failures: 2 Errors: 0
1) test: CounterTest.testClear (F) line: 41 CounterTest.cpp
expected: 0
but was: 1
2) test: CounterTest.testDecr (F) line: 46 CounterTest.cpp
expected: -1
but was: 0
|
次にやることはもうわかっているね。そう、このふたつの失敗が成功に変わり、
となるように Counter.cpp のハリボテ実装部を置き換えるのさ。
テストファーストデザインはちょっとずつ、書いてはテストを繰り返す。
を確認しながら、すべてのテスト項目に対して期待通りに成功することを目指すんだ。少しずつ、ね。