GDBによるテスト自動化への試み

…GDBの機能を使用すると,テストを自動化できないだろうか…そんな疑問がよぎったのでちょっと試してみた.
はじめに
基本方針
デバッガによるテスト
自動化へ向けて
コマンドファイルの作成
量産
もう一越え

はじめに

まず,最初にこの頁はCによる開発を前提にしていることを断っておく.テストというと,最終的には実際に出来上がったものを対象に外部使用をチェックすることになる.しかし,そこに至るまでに関数単位でのテストをボトムアップでやっておかないと,障害の切り分けが繁雑になったり,モノができあがってから「この関数に問題があるので全体の構造をかえる必要がある」なんてことがわかっても困る.ということで,この頁では関数単位のテスト,すなわち関数の呼び出しとその結果のチェックを自動化することを目指してみる.

基本方針

残念ながらテストの自動化といっても,全てが自動なわけではなく,テストを自動的に行なってくれる何かを作成することになる.それはテスト対象とリンクされるプログラムかもしれないし,外部から入出力をチェックするスクリプトかもしれない.ここでは,GDBのコマンドファイルを使用する.入門編でも触れたが,このファイルには通常GDBのプロンプトから入力するコマンドの並びが記述されている.GDBは起動時に.gdbinitファイルがあればそれを実行する.もしくは-xで指定したファイルを実行する.あるいはGDBのプロンプトで
source コマンドファイル
とすることで実行させたりもできる.今回はこのコマンドファイルでテストを自動化しようと思う.メリットは,
ソースを汚さない
問題が検出されて自動テストが停止したとき,そこはもうデバッグ環境である.
こんなところだろうか.

デバッガによるテスト

つまらないものですが,テスト対象のサンプル.

#include <stdio.h>
#include <stdlib.h>

#ifdef TEST
#include "myassert.h"
#endif

char *getCode (int);

int
main (int argc, char **argv)
{
  int param;
  char *code;

  scanf ("%d", &param);

  code = getCode (param);

  fprintf (stdout, "%s\n", code);

  exit (EXIT_SUCCESS);
}

char *
getCode (int target)
{
  switch (target)
    {
    case 1:
      return "HOGE";
      break;
    case 2:
      return "FUGA";
      break;
    case 3:
      return "FOO";
      break;
    case 4:
      return "BAR";
      break;
    default:
      return "FOOBAR";
      break;
    }
}
このサンプルは起動後にユーザの入力を受けて,それに対応した文字列を出力する.1なら"HOGE",2なら"FUGA"....といった具合だ.ここでのテスト対象はgetCode関数である.これをgdbでテストしてみる.まず準備.
> gdb a.out
(gdb) break main
Breakpoint 1 at 0x80484f6: file test-sample1.c, line 16.
(gdb) run
16        scanf ("%d", ¶m;);
そして不要な処理をすべてすっとばしてgetCodeを呼んでみる.
(gdb) call getCode (1)
$1 = 0x804869b "HOGE"
(gdb) call getCode (2)
$2 = 0x80486a0 "FUGA"
(gdb) call getCode (3)
$3 = 0x80486a5 "FOO"
(gdb) call getCode (4)
$4 = 0x80486a9 "BAR"
(gdb) call getCode (5)
$5 = 0x80486ad "FOOBAR"
(gdb) call getCode (6)
$6 = 0x80486ad "FOOBAR"
(gdb) call getCode (0)
$7 = 0x80486ad "FOOBAR"
GDBを使用した手動テストはこんなところだろうか.

自動化へ向けて

さて,前節のテストを自動化するためには何が必要だろうか.デバッガに手動でコマンドを打ち込んだ場合には,getCodeの返り値を目で確認していた.したがって,getCodeに修正が入って次にテストするときにもまた目で確認する必要がある.これを自動化したい.もう一度やってみる.
(gdb) call getCode (1)
$1 = 0x804869b “HOGE”
このとき
(gdb) print $
$2 = 0x804869b “HOGE”
である.$は最後に出力された値が格納されている.で,出力の見た目に違わず,
(gdb) print $
$2 = 0x804869b “HOGE”
(gdb) if $ != “HOGE”

print “NG\n”
end
$3 = “NG\n”
出力のチェックに失敗している.しばらくいろいろと試してみたが,文字列,というかポインタの参照先の値のチェックはGDBだけでは厳しいようだ.そこで,文字列の値のチェックぐらいは自分で作成してみる.一応ヘッダも作ってみた.

#ifndef MYASSERT_H
#define MYASSERT_H

extern int assertString (char *, char *);

#endif

で定義.

#include <string.h>

int
assertString (char *correct, char *checked)
{
  int correct_len;
  int checked_len;

  correct_len = strlen (correct);
  checked_len = strlen (checked);

  if (correct_len != checked_len)
    {
      return 1;
    }

  if (memcmp (correct, checked, correct_len))
    {
      return 1;
    }

  return 0;

}

assertStringは,二つの文字列へのポインタを受取り,両者が一致するなら0,一致しないなら1を返す.これは先のtest-sample1.cのオブジェクトとリンクしさえすれば,コードから呼び出す必要はない.GDBプロンプトからcallするだけである.

gcc -DTEST -Wall -g test-sample1.c myassert.c
gdb a.out
(gdb) break main
Breakpoint 1 at 0x80484f6: file test-sample1.c, line 16.
(gdb) run
(gdb) call getCode (1)
$1 = 0x804869b “HOGE”
(gdb) call assertString (“HOGE”, $)
$2 = 0
(gdb) if $ == 1
print “NG\n”
end
うまくいっているようだ.if文の確認のためにあえてassertStringの引数を間違えてみる.
(gdb) call getCode (2)
$3 = 0x80486a0 “FUGA”
(gdb) call assertString (“HOGE”, $)
$4 = 1
(gdb) if $ == 1
print “NG\n”
end
$5 = “NG\n”
うむ.これで自動化できそうだ(※).

※ 本節は要点がわかりにくいかもしれないので注…もちろん文字列の比較をするだけならstrcmpをcallすればすむ.構造体だったらmemcmpですむかもしれない.「テストの自動化にあったら便利だな」という関数を作成し,リンクしておいてデバッガから呼び出すという方法によって,テスト対象のソースを汚すことなくより柔軟にテストを自動化することが要点である.

コマンドファイルの作成

では,いよいよ自動化.まず,どんなテストを自動化するのか実際にGDBプロンプトからやってみる.
(gdb) break main
Breakpoint 1 at 0x80484f6: file test-sample1.c, line 16.
(gdb) run
(gdb) call getCode (1)
$1 = 0x804869b "HOGE"
(gdb) call assertString ("HOGE", $)
$2 = 0
(gdb) if $ == 1
 >print "test 1 failed\n"
 >end
よさそうだ.保存する.
(gdb) set history filename test-pattern1
(gdb) set history save on
(gdb) quit
できたファイル.
break main
run
call getCode (1)
call assertString ("HOGE", $)
if $ == 1
printf "test 1 failed\n"
end
set history filename test-pattern1
set history save on
quit
最後のsetコマンド二つは消しておかないとこのファイルが消える危険があるので消しておくこと.で,getCodeに修正が加わった後,またテストしたくなったら,
> gdb a.out -x test-pattern1
Breakpoint 1, main (argc=1, argv=0xbffff6a4) at test-sample1.c:16
16        scanf ("%d", ¶m;);
$1 = 0x804869b "HOGE"
$2 = 0
試しにわざとソースを間違えてみる.HOGEをHOOGEとする.
> gdb a.out -x test-pattern1
Breakpoint 1, main (argc=1, argv=0xbffff6a4) at test-sample1.c:16
16        scanf ("%d", ¶m;);
$1 = 0x804869b "HOOGE"
$2 = 1
test 1 failed
うん.思惑どおり.

量産

GDBによるテストの自動化の枠組はここまでの手順でいけそうだ.実際にGDBプロンプトでゴチョゴチョやって,最後にそれら履歴を保存.そして保存したファイルを編集する.今回のテスト対象のようなパターンでは,保存したファイルからコピペでテストパターンを量産できる.

break main
run

call getCode (1)
call assertString (“HOGE”, $)
if $ == 1
printf “test 1 failed\n”
end

call getCode (2)
call assertString (“FUGA”, $)
if $ == 1
printf “test 3 failed\n”
end

call getCode (3)
call assertString (“FOO”, $)
if $ == 1
printf “test 3 failed\n”
end

call getCode (4)
call assertString (“BAR”, $)
if $ == 1
printf “test 4 failed\n”
end

call getCode (5)
call assertString (“FOOBAR”, $)
if $ == 1
printf “test 5 failed\n”
end

call getCode (0)
call assertString (“FOOBAR”, $)
if $ == 1
printf “test 4 failed\n”
end

quit
これを実行してみる.

gdb a.out -x test-pattern1
Breakpoint 1, main (argc=1, argv=0xbffff6a4) at test-sample1.c:16
16 scanf (“%d”, ¶m;);
$1 = 0x804869b “HOOGE”
$2 = 1
test 1 failed
$3 = 0x80486a1 “FUGA”
$4 = 0
$5 = 0x80486a6 “FOO”
$6 = 0
$7 = 0x80486aa “BAR”
$8 = 0
$9 = 0x80486ae “FOOBAR”
$10 = 0
$11 = 0x80486ae “FOOBAR”
$12 = 0
先にわざといれた間違い(HOGEをHOOGEとした)が検出されてはいるが,出力が繁雑でなんだか気に入らない.しかも失敗しても次の項目に移るのもなんだか頂けない.失敗したらそこで止まるようにしたい.これには,GDBコマンドのif文のパターンを以下のようにする方法を考えてみた.
if $ == 1
failedN
end
Nにはテストの項目番号(1,2,3,…)をいれて,どこで失敗したか分かりやすいようにしておく.
gdb a.out -x test-pattern1
Breakpoint 1, main (argc=1, argv=0xbffff6a4) at test-sample1.c:16
16 scanf (“%d”, ¶m;);
$1 = 0x804869b “HOOGE”
$2 = 1
test-pattern1:8: Error in sourced command file:
Undefined command: “failed1”. Try “help”.
(gdb)
failedは実際には存在しないGDBコマンドであれば何でもよい.これによってコマンドファイルの実行を無理矢理止める.GDBは起動したままなのでそのままデバッグに移れる.逆にGDBを抜ければテストはすべて通過したということである.

もう一越え

テストプログラムを本気でテストするわけにもいかないので,テストプログラムは冗長になろうとも短絡的に記述すべきである.その方がどんなテストで失敗したかも分かりやすい.上の例でいくと,人工配列とwhileで簡潔に記述したい衝動に駆られるが,そういうことはテストプログラムですべきではない.ただ,汎用的に使用できそうなifのパターンはユーザ定義コマンドとしてもよいだろう(要はバランスの問題なのだ).
define checkresult
if $ == 1
failed$arg0
end
end
これにより,コマンドファイルは最終的に以下のようになる.
define checkresult
if $ == 1
failed$arg0
end
end

break main
run

call getCode (1)
call assertString (“HOGE”, $)
checkresult 1

call getCode (2)
call assertString (“FUGA”, $)
checkresult 2

call getCode (3)
call assertString (“FOO”, $)
checkresult 3

call getCode (4)
call assertString (“BAR”, $)
checkresult 4

call getCode (5)
call assertString (“FOOBAR”, $)
checkresult 5

call getCode (0)
call assertString (“FOOBAR”, $)
checkresult 6

quit
テストプログラムとしてなかなか良い「雰囲気」ではないだろうか.実行.

gdb a.out -x test-pattern1
Breakpoint 1, main (argc=1, argv=0xbffff6a4) at test-sample1.c:16
16 scanf (“%d”, ¶m;);
$1 = 0x804869b “HOOGE”
$2 = 1
test-pattern1:12: Error in sourced command file:
Undefined command: “failed1”. Try “help”.
ソースを直して再実行.
gdb a.out -x test-pattern1
Breakpoint 1, main (argc=1, argv=0xbffff6a4) at test-sample1.c:16
16 scanf (“%d”, ¶m;);
$1 = 0x804869b “HOGE”
$2 = 0
$3 = 0x80486a0 “FUGA”
$4 = 0
$5 = 0x80486a5 “FOO”
$6 = 0
$7 = 0x80486a9 “BAR”
$8 = 0
$9 = 0x80486ad “FOOBAR”
$10 = 0
$11 = 0x80486ad “FOOBAR”
$12 = 0

無事テストが終了し,GDBを抜けている.

This article was written by Fujiko