CppUnit(1.9.6)と子河童/小馬によるテストファーストデザイン

※ 付録: 魔河童 : 子河童/子馬 Visual Studio (VC++6) add-in ※
$Date: 2002/06/06 02:16:01 $
河童プロジェクト επιστημη

インタフェースを決めよう

さて、君は今、'カウンタ:Counter'を作ってくれと頼まれた。クライアントさんが言うには、そのカウンタは次の2つの要件を満たして欲しいとのこと:
  1. 初期値が0であること
  2. カウント値をインクリメント(+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_;
}

コンパイル/実行しよう:

実行結果

..


OK (2 tests)

よっしゃ、これでCounterの一丁上がりさ。


テストを追加しよう

やれやれ、クライアントが機能追加を要求してきたぞ。そんなの最初に言ってくれよ...

  1. カウント値を0クリアできること
  2. カウント値をデクリメント(-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

次にやることはもうわかっているね。そう、このふたつの失敗が成功に変わり、

実行結果

....


OK (4 tests)

となるように Counter.cpp のハリボテ実装部を置き換えるのさ。

テストファーストデザインはちょっとずつ、書いてはテストを繰り返す。

を確認しながら、すべてのテスト項目に対して期待通りに成功することを目指すんだ。少しずつ、ね。


河童アドイン 「魔河童」 の使い方

2002/12/5 安藤崇紘

紹介

はじめに

魔河童は、もともと僕の会社のピラミッド社内で使うために作りました。ですから、現在も、「ピラミッド用仕様」が数多く存在します。「なんだ?この機能は?」と思う機能がありましたら、それは「それ」なので、笑って許してやってください。

動作環境

動作が確認されている環境は、以下のとおりです。下記以外の環境で動作が確認されましたら、ご連絡ください。また、動作しない場合は、状況をお知らせいただけると、ありがたいです。

OS Windows2000
VCバージョン 6

使ってみる

インストール

まずは、何はともあれ、インストールをしましょう。

  1. cocuppa.exe, couma.exe をパスのとおったディレクトリに配置してください(魔河童はその内部から子河童/子馬を起動します)。macuppa.dll はどこにおいてもかまいません。
  2. VC6 で設定をしましょう。「ツール」→「カスタマイズ」→「アドインおよびマクロファイル」→「参照」の順でmacuppa.dll をアドインに追加します。

  3. 歯車ボタンを押して、設定しましょう。いろいろいじってみてください。

子河童でテストを作ろう

テストケースを追加したいプロジェクトをアクティブにして、河童巻きボタンを押してください。ダイアログが現れるので、適当に項目を追加しましょう。このとき重要なのは「テストクラス名」を必ず入力することです。それ以外は、別に何を打ってもそれなりに動作します。ここでは、テストクラス名が「Test」であるとして話を進めます。準備ができたら、「OK」を押すと...
ほら、「Test.cpp」が表示されて、アクティブプロジェクトに「Test.cpp」が追加されました。簡単ですね。
各入力の初期値は「設定」で変更することができます。いろいろ試してみてください。

ごめんなさい、子河童はTestRunnerすなわちメインルーチンを吐くオプションをサポートしていますが、 魔河童からメインルーチンを生成させることができないんです。面倒ですがコマンドラインに降りて cocuppa --main xxx するか、もしくは前章のrun.cppをパクってください。たった一度のことですから。

子馬もおんなじ

変更したいテストの .cpp ウインドウをアクティブにして、鉄火巻きボタンを押すと子馬が実行できます。追加する関数名や、インクルードファイルを指定したら「OK」ボタンで、はい!出来上がり。

C++スケルトン

う〜、こりゃあ、はっきり言ってピラミッド専用なんですよ。でも、とりあえず使い方を説明します。ひょっとしたら、あなたのお役に立てるかも...。
まず、いきなり「テストファーストしてね」というコーチからのお願いが出ますので、やっぱりテストファーストしようと思ったら、「テストファーストボタン」を押してください。子河童でテストが作れます。そのあとは、必要な条件を入力して、「OK」ボタンを押すだけなのですが、子河童よりも項目が多いので、解説します。

.h, .cpp を、include,src に分けて配置
たとえば、ターゲットクラス名に Target と書いて、ディレクトリのところに ando/takahiro と書いたとすると、このチェックボックスが on になっている場合、./src/ando/takahiro/Target.cpp と、./include/ando/takahiro/Target.h が作られます。逆に off にすると、./ando/takahiro/Target.cpp と、 ./ando/takahiro/Target.h が作られます。
.h 作成 .inl 作成 .cpp 作成
cppgen は、.h, .inl .cpp を作るかどうか選べます。ほとんどの場合は、.h, .cpp を作りたいと思うのですが、.h だけ作りたいとか、.cpp だけ作りたいとか、.inl も作りたいとか、いろいろあると思います。そこで、この機能です。作りたいものを、チェックしてください。「.h, .cpp を、include,src に分けて配置」がチェックされていると、.h, .inl は同じディレクトリに、.cpp はソースのディレクトリに作られます。チェックされていなければ、全て同じディレクトリに作られます。
インラインファイル
「なぜこんなものが?」と、お思いの方に、ご説明します。ピラミッドでは独自のリソースモニターを持っているので、new, delete を、#ifdef _DEBUG... のデバッグ new の定義以降に配置しなければいけません。そこで、.inlファイルを多用しています。そのため、.inl はどこででもインクルードして言い訳ではないので、このような機能をつけました。
Kent Beck の絵と「テストファーストボタン」
メンバーと自分にテストファーストを促すものです。Kent Beck の絵は何かこれに変わるものを募集中です。奮ってご参加ください。あと、設定でのビットマップ変更機能も検討しています。

設定をしよう

このダイアログでは各機能ダイアログの初期値を設定できます。
「ピラミッド社内設定」ボタンが気になることでしょう。これは、ボタンひとつでこのツールをピラミッド仕様にするという恐るべきボタンです。それ以外はいたって普通、それぞれのダイアログの各項目に対応しています。入力が完了したら「OK」ボタンを押してください。
VC終了時に各ダイアログの初期値がレジストリに登録されます。