ホーム < ゲームつくろー! < デバッグ技術編

その5 テスト自動化のためのVisualStudioプロジェクトのコマンドラインビルド



 ゲームに限らずプログラムでは必ず「サブルーチン化」が必要になります。サブルーチンとは小分けされた処理、ぶっちゃげれば「関数」とか「クラス」です。サブルーチン化する一番の理由は「同じ処理を何度も使うから」です。描画処理を100箇所に書くのと1個の関数にまとめて100回コールするのとでは、保守の面で雲泥の差が出るわけです。

 さて、そういうサブルーチンとかクラスなどは正しく動いてくれないと困ります。呼び出し元は「正しく動くにちがいねえ」と思って呼ぶわけで、まさか結果が違うとは思ってもいないわけです。例えば、2つの値を掛け算する関数mulがあったとして、呼び出し元は掛け算されると思って呼びまくるわけです。でも関数の中身が「足し算」になっていたら、戻ってくる結果は全然違うわけで、結果プログラムはめちゃくちゃな動きになってしまいます。「サブルーチンは正しいこと」、これはプログラムの根幹の原則なんです。

 では、本当にサブルーチンは正しいのか?これを検証するのが「単体テスト」「ユニットテスト」と呼ばれるテストプログラムです。例えば先ほどの掛け算関数mulが正しいかを次のようなプログラムでテストします:

掛け算関数のテストプログラム
int mul(int v0, int v1) {
    return v0 + v1; // 足し算になってるぅ!
}

int _tmain(int argc, _TCHAR* argv[])
{
    // 掛け算関数のテストプログラム
    int v0, v1, res;

    v0 = 6;
    v1 = 8;
    res = mul(v0, v1);
    if (res != 48)
        printf("■ mul関数が正しい値を返しませんでした。mul(%d, %d), T(%d), E(%d)\n", v0, v1, v0 * v1, res);

    return 0;
}

テスト値(v0とv1)を用意して、対象とする関数(mul関数)に与えてその答え(res)を得ます。テストする方は「何が正しいのか」を知っているので、関数が思った値をちゃんと返しているか条件文で判断できます。もしmul関数が正しい値を返していなければ、上の例のようにエラーがストリームに出力されます:

 単体テストの書き方は千差万別ですが、間違いを何らかの形で目に出来るようにする点は一緒です。ユニットテストを統一して記述するためのライブラリもあります(CppUnit)。好きな方法でテストプログラムをガンガン書けば、それだけ安心できる領域が広がります。

 さてさて、ここでお話ししたいのはそういう単体テストを沢山書いた後の事です。「単体」とあるように、単体テストは関数やクラスを作成した人が個別で作る事が殆どのため、結果テストプロジェクトが沢山できることになります。そういう状況で、例えばプログラムを最適化(リファクタリング)したとしましょう。当然最適化前と後とで同じ動作をしているかをチェックしなければなりません。その時、関連する単体テストのプロジェクトを立ち上げ直して、ビルドして、実行して確認して…を全部のテストで繰り返すという訳にはいきません。テストプロジェクトが100とかあったら、チェックだけで日が暮れてしまうわけです。

 ではどうしたいかとなります。やりたいのは「テストの自動化」です。プログラマは何らかの機構をポチッとなっと走らせます。すると、テストプロジェクトがリコンパイルされます。場合によってはコンパイルが失敗することもあるでしょう。成功したらテストプログラムが走ります。結果が画面もしくはログに出力されます。これが全部のテストプロジェクトに対してどんどん進んで行きます。プログラマはその間自分の仕事をしてられます。その内再テストが終了するので、プログラマはその結果をみて「よしよし、ふふふ」と思うか「やっべ!何かバグ入れちゃった」と焦るかします(^-^;。

 そういうテストの自動化をする時のネックは実は「GUI操作」です。マウスでクリックして起動するとかビルドボタンを押すなど、人の手が絡む操作は自動化の大敵になってしまうわけです。ですから、自動化を実現するにはやりたい事が「コマンドライン」で操作できなければなりません。コマンドラインならば、実行コマンドを列挙してバッチを動かすだけで自動的に操作が進むからです。

 つまり、テストを自動化するには「Visual Studioのプロジェクトを立ち上げてビルドして実行してログを出力する」というのをコマンドラインから出来るようにする必要があるわけです。



@ msbuild.exeによるコマンドライン実行

 多分、具体的に一からやった方が覚えると思いますので、VisualStudioのプロジェクトを作る所からやってみますね。まず、VisualStudioを立ち上げて新規のプロジェクトを作ります。簡単のためにコンソールアプリケーションにしましょう。フォルダは適当で大丈夫ですが、今はC:\workにしておきます。そこに「Test」というプロジェクトを作りました。

 main.cppを追加して(Test.cppをリネームしてもOK)、こんな感じのプログラムを作成します:

Test/main.cpp
#include "stdafx.h"

int mul_dummy(int v0, int v1) {
    return v0 * v1; // ちゃんと掛け算にします
}

int _tmain(int argc, _TCHAR* argv[])
{
    // 掛け算関数のテストプログラム
    int v0, v1, res;

    v0 = 6;
    v1 = 8;
    res = mul(v0, v1);
    if (res != 48)
        printf("■ mul関数が正しい値を返しませんでした。mul(%d, %d), T(%d), E(%d)\n", v0, v1, v0 * v1, res);

    return 0;
}

先ほどの掛け算プログラムですが、太文字の所を変更してわざとビルドが通らないようにしています。この状態でビルドすると当然コンパイルエラーが出ます:

jコンパイルエラー
1>------ ビルド開始: プロジェクト: Test, 構成: Debug Win32 ------
1>コンパイルしています...
1>main.cpp
1>c:\work\test\main.cpp(14) : error C3861: 'mul': 識別子が見つかりませんでした

この状態で一旦VSを終了します。これでc:\work\Testフォルダ下にTest.slnが一式出来ているはずです。

 次に、もう一つ新しいプロジェクトを立ち上げましょう。同じようにc:\work下に今度はHogeというコンソールアプリケーションを新規追加します。同様にmain.cppを作り次のコードをコピペします:

Hoge/main.cpp
#include "stdafx.h"

int mul(int v0, int v1) {
    return v0 + v1; // 足し算になってるぅ!
}

int _tmain(int argc, _TCHAR* argv[])
{
    // 掛け算関数のテストプログラム
    int v0, v1, res;

    v0 = 6;
    v1 = 8;
    res = mul(v0, v1);
    if (res != 48)
        printf("■ mul関数が正しい値を返しませんでした。mul(%d, %d), T(%d), E(%d)\n", v0, v1, v0 * v1, res);

    return 0;
}

今度はmul関数に戻しますが足し算になっています。実行時にエラーが出るはずです。これはビルドは通るはずです。一度ビルドしてからVSを終了します。

 この段階でC:\workフォルダ下には「Test」と「Hoge」という2つのフォルダが出来ていて、その下にTest.slnとHoge.slnの2つのソルーションが出来ていると思います。このソルーションを指定してコマンドラインからビルドを動かします。それをしてくれるのが「msbuild.exe」です。

 msbuild.exeはMicrosoft.Netにコンポートされているビルドアプリです。これを使ってコマンドラインビルドを回します。まぁ、四の五の言わずに次のように手配をしてみましょう。

 まず、workフォルダに次のようなバッチファイル(AutoTest.bat)を一つ作ります:

work/AutoTest.bat
echo off
echo -- まるぺけTest -- > log.txt

echo ■Test.sln >> log.txt
%windir%\Microsoft.NET\Framework\v2.0.50727\msbuild /nologo /p:Configuration=Release /t:Build Test\Test.sln
if errorlevel 1 echo ■ビルドに失敗しています(T-T)g■ >> log.txt
Test\Release\Test.exe >> log.txt

echo ■Hoge.sln >> log.txt
%windir%\Microsoft.NET\Framework\v2.0.50727\msbuild /nologo /p:Configuration=Release /t:Build Hoge\Hoge.sln
if errorlevel 1 echo ■ビルドに失敗しています(T-T)g■ >> log.txt
Hoge\Release\Hoge.exe >> log.txt

pause

最初のecho offは余計な出力を抑えます。次に空のログファイルを作成して、「-- まるぺけTest --」という名前を付けています。もちろん適当です。次からが本番。まずどのソルーションをビルドし用としているかをログファイルに追加出力しています。肝心のmsbuild.exeは「%windir%\Microsoft.NET\Framework\v2.0.50727\msbuild.exe」というパスにあります。色々なオプションを付けられるのですが、上にあるように/pでDebugとかReleaseなどのコンフィギュレーションを指定できます。/t:Buildでビルド指定となります。リビルドしたい場合は/t:Rebuildです。次にソルーションへのパスを指定します。その後の「if〜」の所はエラー(ビルドエラー等)が検出された時にログにその旨を知らせるメッセージを出力しています。ビルドの後できた.exeを実行しログ出力しています。全く同じ事を今度はHoge.slnに対しても行なっています。

 このバッチを実行すると、次のように出力されます:

わ〜い、コマンドラインビルドが出来ました〜。各プロジェクトのビルドが横線で区別されているので見やすいです。「プロジェクト」の所にどのプロジェクトをビルドしたのかが書かれています。赤文字が大注目するところです。これはエラーが出ている所になります。これを見るとmain.cppの14行目にmulという関数を呼び出しているけども、そんな関数が無いよ〜と怒られています。全く同じエラーメッセージがさっき出ましたよね!結局Test.slnは1つのビルドエラーで終わりました。Test/ReleaseフォルダにTest.exeが作成されていないので「そんな物はねぇ」というエラーも出ています。

 続いてHoge.slnのビルドが走っています。こちらは構文的には正しいコードなのでビルドに成功しています。赤文字が無くて平和です。Hoge/Release/Hoge.exeが出来ているので、それが実行されて出力結果がlog.txtに出ているはずです。ではlog.txtを見てみましょう:

work/AutoTest.bat
-- まるぺけTest --
■Test.sln
■ビルドに失敗しています(T-T)g■
■Hoge.sln
■ mul関数が正しい値を返しませんでした。mul(6, 8), T(48), E(14)

Test.slnはビルドに失敗したそうです(ショボン)。Hoge.slnはビルドには成功してテストが動いたのですが、残念ながらテストに不合格になっています。これを見て「おおっと!?んじゃ、Test.slnとHoge.slnを調べますか」と判断できるようになるわけです。

 Test.slnを開いて改めてビルドを回せば、コードのエラーが直ぐに分かります(DOS窓にも出てますけどね)。Hoge.slnの場合はmul関数が正しい値を出力しなかったわけですから、関数の中身を調べ「足し算になっとるわ(怒)!」とわかるわけです。

 双方を修正して再び先ほどのバッチを叩くとこうなります:

やっほ〜い。で、ログファイルの中身もこうなります:

work/AutoTest.bat
-- まるぺけTest --
■Test.sln
■Hoge.sln

はい、ゴキゲン〜(^-^)。

 という事で、これでコマンドライン上からテストプログラムを一気にビルドして一気に走らせる事が出来るようになりました。バッチを叩けば終わるのでとてつもなく簡単です!

 ただ、このバッチファイルを作るのがちょっと面倒かもしれません。ふふふ、ご安心を、このバッチファイルすらも生成してしまうのですよ、ふふふ。



A AutoTest.batも自動生成だ!

 今AutoTest.batさえ記述できればテストが自動化できる所まで来ました。テキスト文なのでルールさえ分かれば自動的に作れます。では、先ほどのAutoTest.batをもう一度見てみましょう:

work/AutoTest.bat
echo off
echo -- まるぺけTest -- > log.txt

echo ■Test.sln >> log.txt
%windir%\Microsoft.NET\Framework\v2.0.50727\msbuild /nologo /p:Configuration=Release /t:Build Test\Test.sln
if errorlevel 1 echo ■ビルドに失敗しています(T-T)g■ >> log.txt
Test\Release\Test.exe >> log.txt

echo ■Hoge.sln >> log.txt
%windir%\Microsoft.NET\Framework\v2.0.50727\msbuild /nologo /p:Configuration=Release /t:Build Hoge\Hoge.sln
if errorlevel 1 echo ■ビルドに失敗しています(T-T)g■ >> log.txt
Hoge\Release\Hoge.exe >> log.txt

pause

赤文字の所、ここは全く同じ文章です。違うのは太文字のソルーション名のみ。と言う事は、バッチファイルがあるフォルダ下にあるソルーションをチェックして、その名前(とパス)を得られれば、上のような文字列を作れそうです。

 テキストを自動生成する方法は色々とありますが、ここではJScriptを使いたいと思います。理由は簡単だから〜(^-^)

 CreateAutoTest.jsというテキストファイルを作り、次のようなスクリプトを作りました:

work/CreateAutoTest.js
var wsh = new ActiveXObject("WScript.Shell");
var fso = new ActiveXObject("Scripting.FileSystemObject");

// folderPath以下をトラバースしてファイル名をかき集める
function CheckFolder(filePathAry, folderPath, collectFunctor) {
    var folderObj = fso.GetFolder(folderPath);

    // ファイル名列挙
    var files = new Enumerator(folderObj.Files);
    for (; !files.atEnd(); files.moveNext()) {
        var filePath = fso.GetAbsolutePathName(files.item());
        if (collectFunctor(filePath) == true)
            filePathAry.push(filePath);
    }

    // サブフォルダへ
    var folders = new Enumerator(folderObj.SubFolders)
    for (; !folders.atEnd(); folders.moveNext()) {
        CheckFolder(filePathAry, folderPath + "\\" + folders.item().Name, collectFunctor);
    }
}

// .slnのみ
function extFunctor(filePath) {
    var extArray = ["sln"];
    var ext = fso.GetExtensionName(filePath);
    for (var i = 0; i < extArray.length; i++)
        if (ext == extArray[i])
            return true;
        return false;
    }


// カレントディレクトリ取得
var rootDir = wsh.CurrentDirectory;
var filePathAry = new Array;
CheckFolder(filePathAry, rootDir, extFunctor); // <- Fuctorでフィルター掛けられます

// 収集したファイルフルパスチェック
for (var i = 0; i < filePathAry.length; i++)
    WScript.Echo(filePathAry[i]);

// バッチファイル作成
var code = new String;
code = "echo off\r\n"
code += "echo -- まるぺけTest -- > log.txt\r\n\r\n"

for (var i = 0; i < filePathAry.length; i++) {
    // ベース名
    var baseName = fso.GetBaseName(filePathAry[i]);
    // .slnパス
    var slnPath = filePathAry[i];
    code += "echo ■" + baseName + ".sln >> log.txt\r\n";
    code += "\%windir\%\\\Microsoft.NET\\Framework\\v2.0.50727\\msbuild /nologo /p:Configuration=Release /t:Build " + slnPath + "\r\n";
    code += "if errorlevel 1 echo ■ビルドに失敗しています(T-T)g■ >> log.txt\r\n";
    code += baseName + "\\Release\\" + baseName + ".exe >> log.txt\r\n";
    code += "\r\n";
}

code += "\r\npause";

var file = fso.OpenTextFile("AutoTest.bat", 2, true, 0);
file.Write(code);
file.Close();

はい、このスクリプトを実行するバッチファイル(CreateAutoTest.bat)をさらに作成します:

work/CreateAutoTest.bat
cscript CreateAutoTest.js
pause

CreateAutoTest.batを実行すると、サブフォルダにあるソルーションファイル名をかき集めてきます。次にその名前とパスを使ってテスト自動生成なバッチファイル(AutoTest.bat)を作成してくれます。

 作成されたバッチファイルはこんな感じです:

work/AutoTest.bat
echo off
echo -- まるぺけTest -- > log.txt

echo ■Hoge.sln >> log.txt
%windir%\Microsoft.NET\Framework\v2.0.50727\msbuild /nologo /p:Configuration=Release /t:Build C:\work\TestPrj\Hoge\Hoge.sln
if errorlevel 1 echo ■ビルドに失敗しています(T-T)g■ >> log.txt
Hoge\Release\Hoge.exe >> log.txt

echo ■Test.sln >> log.txt
%windir%\Microsoft.NET\Framework\v2.0.50727\msbuild /nologo /p:Configuration=Release /t:Build C:\work\TestPrj\Test\Test.sln
if errorlevel 1 echo ■ビルドに失敗しています(T-T)g■ >> log.txt
Test\Release\Test.exe >> log.txt


pause

一緒です(^-^)。

 これで、特定のフォルダにテストプロジェクトを追加してもCreateAutoTest.batを叩くだけで専用のテスト自動バッチ(AutoTest.bat)が作成されます。AutoTest.batを叩けばもうテストが走り出しますし、ビルドも更新されるので常にテストプロジェクトが新鮮な(コンパイルされる)状態で保てます!


 今回作ってきたCreateAutoTest.jsとCreateAutoTest.batをこちらで公開します。使い方は簡単。ワークフォルダにコピーして、そのフォルダの下に.slnがあるプロジェクトフォルダを置いてバッチを叩くだけです。それでAutoTest.batが出来上がりますので、それをさらに叩けばビルドと実行が行われます。


 こういうテストの自動化は大規模なプロジェクトになればなるほど必須になってきます。早めに環境を整えることをお勧めです。