2012年09月15日

N6XBasicChecker(6)ユニットテストの記述

いい加減コードを書く方に戻りたくなってきたので、
N6XBasicCheckerのメイキングは今回で一段落付けたいと思います。
今回はユニットテストの記述についてです。

構文解析機は、その性質上
・パーサージェネレータで生成した構文解析器は、ステップ実行でデバッグするのはほぼ無理。
・構文定義をすべて書ききってからテストをしたのでは、バグの原因の特定が困難。
・コマンドやステートメントの構文定義を追加する行為は、構文解析器の状態遷移の定義を変えることに等しいので、デグレードを起こしやすい。
という特徴があるので、N6XBasicCheckerでは、構文解析器のコーディングと、ユニットテストの記述を同時進行で進めました。
というわけで、N66SR-Basicのコマンドやステートメント、関数は約150程度あるわけですが、
1)構文定義を記述
2)テストコードを追加
3)テストを実行してデグレードを起こしていないことを確認
という作業を150回ほど繰り返しています(;´Д`)

テストフレームワークにはQtに付属のQTestを使っています。
N6XBasicChecker本体はQtに依存していませんが、開発環境とテストにはQtSDKを使っています。
ユニットテストを記述するにはQtCreatorのメニューから「ファイル→ファイル/プロジェクトの新規作成」を選択して「Qtユニットテスト」を選択します。
qtest_wizard.png

テストプログラムのCPPファイルが生成されるので、こいつにテストケースを追加していきます。

class LibN6XBasicCheckerTest : public QObject
{
Q_OBJECT
public:
LibN6XBasicCheckerTest();
private:
bool parse(const std::wstring& program, ParserStatus& stat, bool errorTrace = false, bool warningTrace = false);
private Q_SLOTS:
void testCase11();
void testCase10();
void testCase9();
void testCase8();
void testCase7();
void testCase6();
void testCase5();
void testCase4();
void testCase3();
void testCase2();
void testCase1();
void testCaseX();
};
QTEST_MAIN(LibN6XBasicCheckerTest)

最後のQTEST_MAINというマクロがmain関数を代替しているので、mainの記述は不要です。
テストケースはQtで言うところのプライベートスロットとして関数を追加します。
テストケースの呼ばれる順番は、ヘッダーで宣言された順番です。
テストケースの番号が降順になっていますが、これは
・新しく追加したテストの結果を早く確認するため
・テストケースを追加した際のカーソル移動量を減らすため
で、後から追加したものを上に持ってくるようにしています。
上記のようにテストの反復回数が半端無いので、コンパイルやテストの実行時間を少しでも短くするよう一応工夫をしています。

個々のテストケースは以下のようになります。

void LibN6XBasicCheckerTest::testCase2()
{
ParserStatus stat;
std::wstring programList;
//エラー行判定
programList =
L"10 goto 10: go to10\n"
"20 goo 20\n" //エラー
"\n"
"30 goto 30\n"
"40 goto-40\n" //エラー
;
QVERIFY(!parse(programList, stat));
QVERIFY(stat.errorList_.size() == 2);
QVERIFY(stat.errorList_[0].line_.textLineNumber_ == 2);
QVERIFY(stat.errorList_[0].line_.basicLineNumber_ == 20);
QVERIFY(stat.errorList_[1].line_.textLineNumber_ == 5);
QVERIFY(stat.errorList_[1].line_.basicLineNumber_ == 40);
}

void LibN6XBasicCheckerTest::testCase1()
{
ParserStatus stat;
std::wstring programList;
//正常系
programList =
L"10 goto 10: go to10\n"
"\n"
"20 goto 20\n"
;
QVERIFY(parse(programList, stat, true));
}

テスト中のQVERIFY()で評価した値がfalseになるとエラーになり、テストが停止します。
testCase1()ではパースが正常に終了していること、testCase2()では期待した箇所にエラーが出ていることを確認しています。

最後にあるtestCaseXというのは、Hashiさんのサイトに公開されているベーマガ掲載プログラムリスト(約100本強!)のチェックをするためのテストケースで、一番実戦的なテストです。
リスト自体は再配布するわけに行かないので、テスト時にビルドディレクトリに手でコピーする運用にしています。

void LibN6XBasicCheckerTest::testCaseX()
{
babel::init_babel();
//tst_libn6xbasiccheckertesttestのバイナリと同階層にある
//listというディレクトリ内のBASICリストファイルを順次パースする。
//listディレクトリ内に配置するBASICリストファイルは自分で用意すること。
//Hashiさんのサイトに掲載されているBASICリストをまとめて回帰テストするためのテストケース。
QString listPath = qApp->applicationDirPath() + QDir::separator() + "list";

QDir dir(listPath);
QVERIFY(dir.exists());

QStringList files;
files = dir.entryList(QStringList("*.txt"),
QDir::Files | QDir::NoSymLinks);

foreach(QString file, files){
std::ifstream fst((listPath + QDir::separator() + file).toLocal8Bit());
std::string sjisList((std::istreambuf_iterator(fst)), std::istreambuf_iterator());
std::wstring unicodeList = babel::sjis_to_unicode(sjisList);

ParserStatus stat;
QVERIFY(parse(unicodeList, stat, true, true));
if(!stat.errorList_.empty())
std::cout << ((QString("errors found in ") + file).toStdString()) << std::endl;
if(!stat.warningList_.empty())
std::cout << ((QString("warning found in ") + file).toStdString()) << std::endl;
}
}

実際にやってみると、リファレンスに載ってない構文が使えることが色々わかって興味深かったです。

ブログの更新は一段落付けて、しばらくN6XBasicCheckerの残件(MML対応など)をやりたいと思います。
N6XBasicCheckerの次のリリースでお会いしましょう。
posted by eighttails at 20:28| Comment(0) | N6XBasicChecker | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: