以下は一例である
共通点: | テストデータに従って体系的に行う必要がある |
相違点: | テストデータの作成方法 |
テスト条件を具体化したものであり、それに従ってテストを行うことで プログラムのすべての分岐(パス)において振る舞いが正しいかを確認できなければならない.
factは整数から整数への関数であり、分岐はn==0かそうでないかの1箇所である.
テストデータは配列として保持するのが適当である.
配列には要素数か終了の印が必要であるが、固定長の場合には
配列全体の大きさsizeof(a)を配列の要素1つ分の大きさsizeof(a[0])で
割れば、配列の要素数を求めることができる.
// テスト対象のプロトタイプ宣言 int fact(int); // テストデータ配列 struct { int arg; int expected; } factdata[] = { 0 , 1 , // fact(0)=1 1 , 1 , // fact(1)=1 2 , 2 , // fact(2)=2 -1 , 1, // fact(-1)=1 -2 , 1, // fact(-2)=1 }; // テストデータの個数 int testcnt = sizeof(factdata)/sizeof(factdata[0]); void test_fact() { int result; for (int i=0;i<testcnt;i++) { printf("test %d:",factdata[i].arg); result = fact(factdata[i].arg); if (result != factdata[i].expected) printf("failed\n"); else printf("success\n"); } } int fact(int n) { /* 略 */ } int main() { test_fact(); return 0; }
実行するとtest 0,1,2には成功するが、-1の途中でおそらく異常終了する. これはnが負の場合にはn==0が成立しないまま 再帰を繰り返してメモリ不足になるためである.
nが負でも異常終了しないようにするには、 例えば以下のようにn>0の場合のみ再帰するようにすればよい.
int fact(int n) { int v = 1; if (n > 0) v = n * fact(n-1); return v; }
なおtest_factはfactとfactdataの部分を以下のように引数で与えるように修正すれば、
汎用の単体テスト用関数として利用できる. 配列を関数の引数とするには
ポインタと要素数(コンパイル時に計算される)の
両方を別の引数として渡す必要がある点に注意.
ちなみにargc, argvはargument count(引数の個数), argument vector(引数ベクトル)の
省略形であるが、C言語で配列を扱う際にほぼ慣用句として用いられるので
覚えておくとよい.
(型は異なるが、実はmainの引数も通常はargc,argvと表記する)
// テスト対象関数の型宣言(整数配列=>整数) typedef int (*Func)(int,int*) ; // 配列の要素数 #define CNT(x) (sizeof(x)/sizeof(x[0])) typedef struct { int expected; // 期待される結果 int argc; // 引数の個数 int *argv; // 引数配列 } TestElem; typedef struct { Func testfunc; // 関数 char *funcname; // 関数名 int testcnt; // テストデータ個数 TestElem *testdata; // テストデータ配列 } TestUnit; // テスト対象関数 int add(int,int*); int sub(int,int*); // 引数ベクトル int v0[] = { 2, 1}; int v1[] = { 2, 3}; int v2[] = { 2, 3, 4}; // add用テストデータ TestElem add_elem[] = { 3, CNT(v0), v0, 5, CNT(v1), v1, 9, CNT(v2), v2 }; // sub用テストデータ TestElem sub_elem[] = { 1, CNT(v0), v0, -1, CNT(v1), v1, -5, CNT(v2), v2 }; // テストデータ統合 TestUnit unit[] = { add, "add", CNT(add_elem), add_elem, sub, "sub", CNT(sub_elem), sub_elem }; // 汎用単体テスト関数 void test_unit(int n, TestUnit *u) { for (int i=0;i<n;i++) { Func f=u[i].testfunc; TestElem *p=u[i].testdata; printf("func %s:\n", u[i].funcname); for (int j=0;j<u[i].testcnt;j++) { printf("test %d: ",j); printf("argv=("); for (int k=0;k<p[j].argc;k++) printf(" %d", p[j].argv[k]); printf(") expected= %d :",p[j].expected); int result = (*f)(p[j].argc, p[j].argv); if (result != p[j].expected) printf("failed\n"); else printf("success\n"); } } } // テスト対象の実装 int add(int c,int *v) { int r=v[0]; for(int i=1;i<c;i++) r+=v[i]; return r; } int sub(int c,int *v) { int r=v[0]; for(int i=1;i<c;i++) r-=v[i]; return r; } int main() { test_unit(CNT(unit), unit); return 0; }
最初に関数の実装を示す.
線形リストの作成では先頭はダミー要素となる点に注意.
scanfは読み込んだ変数の個数を返すが、入力が終了すると0を返す.
duplicatedはダミーを除いた先頭から順に2つの要素を比較して、
1組でも一致すればtrueを返す.
このアルゴリズムは効率が悪いが、長さ100個程度までなら実用上十分である.
// 環境によっては不要 typedef enum { false=0, true=1 } bool; // 線形リストの型定義 typedef struct list { int val; struct list *next; } List; // テスト対象のプロトタイプ宣言 void input_list(List*); bool duplicated(const List*); // 入力をリストにして返す void input_list(List *l) { int v; List *p=l; while (scanf("%d", &v)>0) { List *newp = (List*)malloc(sizeof(List)); newp->val = v; p->next = newp; p = p->next; } } // リストの要素の重複を検査する bool duplicated(const List *l) { List *lp = l->next; for (; lp; lp=lp->next) { List *lp2 = lp->next; for (; lp2; lp2=lp2->next) { if (lp->val == lp2->val) return true; } } return false; } List list; int main() { input_list(&list); int d = duplicated(&list); printf("%d\n",d); return 0; }
単体テストを行うにはテストデータの渡し方を検討する必要がある. 現在のinput_listは入力を最後まで読み込んで一つのリストを作成するが、 テストに先立って1行分をひとつのテストデータとして解釈するように 修正する. 行の切り分けは呼び出し側で行い、文字列としてinput_listに 与えることにする.
書式指定の中の%nは読み込んだ文字数を対応する変数(n)に代入する.
sscanf の後 bp+=nで入力ポインタをn文字進めることでscanfと等価になる.
// 入力を文字列で与えるように修正 void input_list_sub(char *buf, List *l) { int v, n; List *p=l; char *bp = buf; while (sscanf(bp, "%d %n", &v, &n) > 0) { List *newp = (List*)malloc(sizeof(List)); newp->val = v; p->next = newp; p = p->next; bp += n; } } // 1行毎にテストデータを読むように修正 void test_input_list() { char buf[256]; while (fgets(buf,256,stdin) != 0) { input_list_sub(buf, &list); int d = duplicated(&list); printf("%d\n",d); } }
注意: 4章で示したように、scanf/sscanfは不正な入力に対して 予期しない動作をする. またscanf/sscanfの%n書式指定は 深刻なセキュリティーホールの原因になりやすい. ここでは単体テストに焦点を 当てるため不正入力を考慮していないが、 実用的なプログラムでは 解答4.3のseparateのような空白の切り出しを行う関数を 自分で作成することを推奨する.
テストデータはファイルに記述し、入力リダイレクトで渡せばよい.
test_list.c inflie1% ./test_list < infile1 0 0 1 %
duplicatedの単体テストはテストデータとして リストを作成して渡す必要があるので、 先にinput_listの単体テストが完了していなければならない.
なお標準入力よりテストデータを読む仕様のテスト関数を、 特定の入力を与えて自動的に呼び出すには、 プログラム中で別のプログラムを実行する仕組みを利用する. 実現方法は環境に依存するが、POSIXではsystem()関数が使用できる.
test_all.c// テスト用コマンド列 char *com_list[] = { "./test_list < infile1", "./test_list < infile2", 0 }; // コマンドの実行 void test_all() { char **cp = com_list; for (;*cp;cp++) { printf("%s\n",*cp); system(*cp); } }
出力リダイレクトを利用してテストの実行結果を別のファイルに格納し、 あらかじめ用意した期待される結果のファイルと (cmpコマンドで)比較を行うこともできる. 各自試してみよ.
getdistanceと単体テストは以下のとおり. 規則の修正は簡単なので省略.
distance.c#define CNT(x) (sizeof(x)/sizeof(x[0])) // 距離の計算 // ここでは(x座標の差)+(y座標の差) int getdistance(Point from, Point to) { int dx = from.x - to.x; int dy = from.y - to.y; return abs(dx)+abs(dy); } // テストデータ struct { int expected; Point from, to; } data[] = { 1 , {3, 3}, {3, 4}, 2 , {3, 3}, {4, 4} }; // テスト関数 void test_getdistance() { for (int i=0;i<CNT(data);i++) { Point f = data[i].from; Point t = data[i].to; int r = getdistance(f,t); if (r != data[i].expected) printf("failed\n"); else printf("success\n"); } }
黒番と白番で現在位置の石の色の条件が異なる.
if文やswitch文で分岐するより、配列を使用した方が簡潔に記述できる.
static Color cl[] = { c_black, c_white }; bool check_rules(Move *mp, Board *bp) { /* ア */ if (getcolor(bp, mp->from) != cl[bp->turn]) return false; /* イ */ if (getcolor(bp, mp->to) != c_empty) return false; for (int i=0; i < MAXRULE; i++) { Rule r = rules[bp->turn][i]; if (r==0) break; if ((*r)(mp,bp)) return true; } return false; }
ただし(将棋のコマのように)移動先に別の石があっても、 それを取って移動できるルールが存在する場合には、 イの判定はループの外に出すことはできないので注意.