CUnit(1.0-8)とちび河童/ちび馬によるテストファーストデザイン

$Date: 2002/08/21 06:26:53 $
河童プロジェクト επιστημη

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

さて、君は今、'カウンタ:counter'を作ってくれと頼まれた。クライアントさんが言うには、そのカウンタは次の2つの要件を満たして欲しいとのこと:
  1. 初期値が0であること
  2. カウント値をインクリメント(+1)できること

腕のたつ君のことだ、もうヘッダを書き始めていることだろう。こんな感じでね:

counter.h

#ifndef __COUNTER_H__
#define __COUNTER_H__

struct counter_t {
  int value;
};

typedef struct counter_t ctr;
typedef ctr* ctrP;

extern ctrP ctr_initialize();    /* 初期化 */
extern void ctr_terminate(ctrP); /* 後始末 */
extern int  ctr_value(ctrP);     /* 今いくつ? */
extern void ctr_incr(ctrP);      /* +1 する */

#endif

上出来だね...で、これだけだとcounterに必要な関数は宣言されてはいるものの実装(定義)されていないから、コンパイルできるけどリンクに失敗するに決まってる。とりあえずリンクまではできるよう、中身カラッポのハリボテ実装を用意しておこう:

Counter.c

#include "counter.h"
#include <stdlib.h>

ctrP ctr_initialize() {
  return (ctrP)malloc(sizeof(ctr));
}

void ctr_terminate(ctrP c) {
  free(c);
}

int ctr_value(ctrP c) {
  return -123; /* テキトーな値 */
}

void ctr_incr(ctrP c) {
  /* 何もしない */
}

このハリボテ実装に手を加え、そしてテストしながらクライアントに呈示された要件を満足する'本物'に近づけていこうってわけだ。


テストの実装を始めよう

テストファーストデザインでは、まずはじめにテストを書く。そしてそのテストが成功するように実装を埋めるんだ。

ちび河童はテストコードの雛形を吐いてくれる小さなツールだ。CコードをCUnitでテストするとき、 テストコードはCUnitが定める'お作法'に従っていなければならない。 ちび河童はそのお作法通りのテストコードを吐いてくれる。 お作法はちび河童にまかせ、君は与えられた要件を満たすことを検証するコードを書けばいいのさ。

早速ちび河童を使ってみよう。ちび河童が吐くのはテストコードだ。 counterをテストするんだからテストコードの名前は'counterTest'としようか。 そしてテスト関数は[1]初期化と[2]インクリメントを検証するのだからそれぞれ'testInit', 'testIncr'でどうだろう。 そしてテストコードはcounter.hを#includeしなきゃいけないね。 counter.h/c を置いたディレクトリでちび河童にひと働きしてもらおう:

tcuppa counter.h counterTest testInit testIncr

ちび河童は counterTest.c を吐いてくれたはずだ:

counterTest.c

#include <CUnit.h>
#include <TestDB.h>

/*CUPPA:include=+ */
#include "counter.h"
/*CUPPA:include=+ */


static int init(void) {
  /* initialize */
  return 0;
}

static int term(void) {
  /* terminate */
  return 0;
}

/*CUPPA:impl=+ */
static void testInit(void) {
  ASSERT(!"no implementation");
}

static void testIncr(void) {
  ASSERT(!"no implementation");
}

/*CUPPA:impl=- */
void counterTest(void) {
  TestGroup* group = add_test_group("counterTest", init, term);
/*CUPPA:suite=+ */
  add_test_case(group, "testInit", testInit);
  add_test_case(group, "testIncr", testIncr);
/*CUPPA:suite=- */
}
吐かれたコードのあちこちに //CUPPA:... っていうワケのわからないコメントが埋め込んである。
こいつらを書き換えないでくれ。

それではコンパイル...ちょっと待って、main()がなくちゃ動かしようがない。ちび河童はmain()を作ることもできるんだ。

bcuppa run counterTest

main()が定義された run.c ができたかな?

run.c

#include <Console.h>
/*CUPPA:groupdecl=+ */
extern void counterTest(void);
/*CUPPA:groupdecl=- */

int main() {
  initialize_registry();
/*CUPPA:groupreg=+ */
  counterTest();
/*CUPPA:groupreg=- */
  console_run_tests();
  cleanup_registry();
  return 0;
}

気を取り直してVisual C++でコンパイルしてみよう。CUnitのインストールディレクトリがd:\CUnitだとすると:

cl -Id:/CUnit/CUnit/Headers run.c counterTest.c counter.c cunit.lib -link -libpath:d:/CUnit/CUnit

これでめでたくrun.exeができたことと思う。実行しよう:

実行結果



                        CUnit : A Unit testing framework for C.
                            http://cunit.sourceforge.net/


|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : r

Running Group : counterTest
        Running test : testInit
        Running test : testIncr

--Completed 1 Groups, 2 Test run, 0 succeded and 2 failed.
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : f

============================= Test Case Failure List =========================
1>  counterTest.c:25 : (counterTest : testIncr) : !"no implementation"
2>  counterTest.c:21 : (counterTest : testInit) : !"no implementation"

=======================================
Total Number of Failures : 2
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : q

..."no implementation"だってさ。 ちび河童が吐いたテストコードそのままでは、必ずこのメッセージを出力して'失敗'する。 これでいいんだ。だってこれでテスト関数 testInitとtestIncrが実行されたことが確認できただろ? それに君はまだ要件を満たすことを検証するコードを一行も書いちゃいない。 実行結果はそのことを君に教えてくれたんだ。 テストの準備が整ったシルシだよ。


テスト関数を実装しよう

じゃぁその検証コード(テスト関数)とやらを書くとしよう。counterTest.cを次のように修正すればいい:
counterTest.c (追加/修正個所をboldで示す)
#include <CUnit.h>
#include <TestDB.h>

/*CUPPA:include=+ */
#include "counter.h"
/*CUPPA:include=+ */

static int init(void) {
  /* initialize */
  return 0;
}

static int term(void) {
  /* terminate */
  return 0;
}

/*CUPPA:impl=+ */
static void testInit(void) {
  ctrP c = ctr_initialize();
  ASSERT( 0 == ctr_value(c) );
  ctr_terminate(c);
}

static void testIncr(void) {
  ctrP c = ctr_initialize();
  ASSERT( 0 == ctr_value(c) );
  ctr_incr(c);
  ASSERT( 1 == ctr_value(c) );
  ctr_incr(c);
  ASSERT( 2 == ctr_value(c) );
  ctr_terminate(c);
}

/*CUPPA:impl=- */
void counterTest(void) {
  TestGroup* group = add_test_group("counterTest", init, term);
/*CUPPA:suite=+ */
  add_test_case(group, "testInit", testInit);
  add_test_case(group, "testIncr", testIncr);
/*CUPPA:suite=- */
}

関数init/termはこのそれぞれこの一連のテストの実行直前/直後に呼び出される。 テストの初期化/後始末ってワケだ。 そしてテスト関数の中に書かれたマクロASSERTは、 その引数を評価し、0(偽)であったらテスト'失敗'として次のテスト関数に移る。 コンパイル/実行しよう:

実行結果


                        CUnit : A Unit testing framework for C.
                            http://cunit.sourceforge.net/


|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : r

Running Group : counterTest
        Running test : testInit
        Running test : testIncr

--Completed 1 Groups, 2 Test run, 0 succeded and 2 failed.
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : f

============================= Test Case Failure List =========================
1>  counterTest.c:28 : (counterTest : testIncr) : 0 == ctr_value(c)
2>  counterTest.c:22 : (counterTest : testInit) : 0 == ctr_value(c)

=======================================
Total Number of Failures : 2
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : q

案の定失敗したね。だってまだハリボテ実装なんだものね。


少しづつ、書いてはテストしよう

では初期化から片付けるとしよう:

counter.c
#include "counter.h"
#include <stdlib.h>

ctrP ctr_initialize() {
  ctrP result = (ctrP)malloc(sizeof(ctr));
  if ( result ) {
    result->value = 0;
  }
  return result;
}

void ctr_terminate(ctrP c) {
  free(c);
}

int ctr_value(ctrP c) {
  return c->value;
}

void ctr_incr(ctrP c) {
  /* 何もしない */
}

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

実行結果

                        CUnit : A Unit testing framework for C.
                            http://cunit.sourceforge.net/


|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : r

Running Group : counterTest
        Running test : testInit
        Running test : testIncr

--Completed 1 Groups, 2 Test run, 1 succeded and 1 failed.
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : f

============================= Test Case Failure List =========================
1>  counterTest.c:30 : (counterTest : testIncr) : 1 == ctr_value(c)

=======================================
Total Number of Failures : 1
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : q

よしよし、失敗がひとつ減ったね。残るはincr()だ:

counter.c
#include "counter.h"
#include <stdlib.h>

ctrP ctr_initialize() {
  ctrP result = (ctrP)malloc(sizeof(ctr));
  if ( result ) {
    result->value = 0;
  }
  return result;
}

void ctr_terminate(ctrP c) {
  free(c);
}

int ctr_value(ctrP c) {
  return c->value;
}

void ctr_incr(ctrP c) {
  ++c->value;
}

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

実行結果

                        CUnit : A Unit testing framework for C.
                            http://cunit.sourceforge.net/


|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : r

Running Group : counterTest
        Running test : testInit
        Running test : testIncr

--Completed 1 Groups, 2 Test run, 2 succeded and 0 failed.
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : q

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


テストを追加しよう

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

  1. カウント値を0クリアできること
  2. カウント値をデクリメント(-1)できること

ああ、わかったわかった。やればいいんだろやれば... できたばかりのCounterにちょっと手を入れよう。でも実装はカラッポにしておく。

counter.h
#ifndef __COUNTER_H__
#define __COUNTER_H__

struct counter_t {
  int value;
};

typedef struct counter_t ctr;
typedef ctr* ctrP;

extern ctrP ctr_initialize();
extern void ctr_terminate(ctrP);
extern int  ctr_value(ctrP);
extern void ctr_incr(ctrP);
extern void ctr_decr(ctrP);
extern void ctr_clear(ctrP);

#endif
counter.c

#include "counter.h"
#include <stdlib.h>

ctrP ctr_initialize() {
  ctrP result = (ctrP)malloc(sizeof(ctr));
  if ( result ) {
    result->value = 0;
  }
  return result;
}

void ctr_terminate(ctrP c) {
  free(c);
}

int ctr_value(ctrP c) {
  return c->value;
}

void ctr_incr(ctrP c) {
  ++c->value;
}

void ctr_decr(ctrP c) {
  /* 何もしない */
}

void ctr_clear(ctrP c) {
  /* 何もしない */
}

次にやることは? もちろん追加分のテストに決まってる。 テスト関数 testClear と testDecr を counterTest.c に追加しなきゃならない

申し訳ないんだが、ちび河童は最初の一歩を踏み出すための雛型を吐いてはくれるけど、 その後新たなテスト関数を追加することはできないんだ。

こんなときにはちび馬を使ってほしい。 ちび河童が吐いたテストコードにテスト関数を追加するのがちび馬のお仕事だ。

tuma counterTest testClear testDecr

これでテストコード counterTest.c にテスト関数 testClear, testDecr が加えられているはずだ。
ちび馬もちび河童と同様、"no implementation"を出力して失敗するテスト関数を吐く。

クリア/デクリメントを検証するコードに置き換えよう:

counterTest.c

#include <CUnit.h>
#include <TestDB.h>

/*CUPPA:include=+ */
#include "counter.h"
/*CUPPA:include=+ */


static int init(void) {
  /* initialize */
  return 0;
}

static int term(void) {
  /* terminate */
  return 0;
}

/*CUPPA:impl=+ */
static void testInit(void) {
  ctrP c = ctr_initialize();
  ASSERT( 0 == ctr_value(c) );
  ctr_terminate(c);
}

static void testIncr(void) {
  ctrP c = ctr_initialize();
  ASSERT( 0 == ctr_value(c) );
  ctr_incr(c);
  ASSERT( 1 == ctr_value(c) );
  ctr_incr(c);
  ASSERT( 2 == ctr_value(c) );
  ctr_terminate(c);
}

static void testClear(void) {
  ctrP c = ctr_initialize();
  ctr_incr(c);
  ASSERT( 0 != ctr_value(c) );
  ctr_clear(c);
  ASSERT( 0 == ctr_value(c) );
  ctr_terminate(c);
}

static void testDecr(void) {
  ctrP c = ctr_initialize();
  ASSERT( 0 == ctr_value(c) );
  ctr_decr(c);
  ASSERT( -1 == ctr_value(c) );
  ctr_decr(c);
  ASSERT( -2 == ctr_value(c) );
  ctr_terminate(c);
}

/*CUPPA:impl=- */
void counterTest(void) {
  TestGroup* group = add_test_group("counterTest", init, term);
/*CUPPA:suite=+ */
  add_test_case(group, "testInit", testInit);
  add_test_case(group, "testIncr", testIncr);
  add_test_case(group, "testClear", testClear);
  add_test_case(group, "testDecr", testDecr);
/*CUPPA:suite=- */
}

コンパイル実行するとこんな結果が得られるだろう:

実行結果

                        CUnit : A Unit testing framework for C.
                            http://cunit.sourceforge.net/


|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : r

Running Group : counterTest
        Running test : testInit
        Running test : testIncr
        Running test : testClear
        Running test : testDecr

--Completed 1 Groups, 4 Test run, 2 succeded and 2 failed.
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : f

============================= Test Case Failure List =========================
1>  counterTest.c:49 : (counterTest : testDecr) : -1 == ctr_value(c)
2>  counterTest.c:41 : (counterTest : testClear) : 0 == ctr_value(c)

=======================================
Total Number of Failures : 2
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : q

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

実行結果

                        CUnit : A Unit testing framework for C.
                            http://cunit.sourceforge.net/


|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : r

Running Group : counterTest
        Running test : testInit
        Running test : testIncr
        Running test : testClear
        Running test : testDecr

--Completed 1 Groups, 4 Test run, 4 succeded and 0 failed.
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(R)un all, (S)elect group, (L)ist groups, Show (F)ailures, (Q)uit
Enter Command : q

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

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

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