はじめに
どうも、迷走星人です。
4回目はプログラム試験の実践部分の話を出来たらと思います。
尚、この段階では以下の工程が完了しているものとします。
プログラムの設計&レビュー
プログラムの実装&レビュー
プログラム試験の設計&レビュー
試験環境準備
プログラム試験1の試験を実施するために、テスト用のプロジェクトを用意します。
テスト用のプロジェクトは、Visual Studio の「ネイティブ単体テストプロジェクト 」で作成します。
テスト用プロジェクトの作成
まずはテスト用のプロジェクト作成をしていきます。作成手順は以下に書いていきます。
プログラムを作成したソリューションから[追加]->[新しいプロジェクト]を選択します。
テスト用プロジェクト作成画面キャプチャ01
2. プロジェクトウィザードから「ネイティブ単体テスト プロジェクト」を選択します。
テスト用プロジェクト作成画面キャプチャ02
3. 自分のPCの任意の場所にプロジェクトを作成します。ここでは「TaikaProj01_CalcGradeRankTest」というプロジェクト名にします。
テスト用プロジェクト作成画面キャプチャ03
4. 単体テスト 用のプロジェクトが作成されました。このテスト用ソースコード にテスト用の実装を進めていきます。
テスト用プロジェクト作成画面キャプチャ04
テスト用プロジェクトの設定
テスト用のプロジェクトが作成出来たら、テスト対象のプロジェクトとリンクするように設定します。
テスト用プロジェクトで、[参照]->[参照の追加]を選択します。
テスト用プロジェクト設定画面キャプチャ01
2. 「参照の追加」ダイアログで[プロジェクト]を選択し、テスト対象のプロジェクトのチェックボックス にチェックを入れます。
テスト用プロジェクト設定画面キャプチャ02
3. テスト用のプロジェクトに今回のプログラム開発で実装したクラスファイルの「*.h」及び「*.obj」格納場所を追加します。また、「*.obj」はファイル名もテスト用プロジェクトに追加します。
テスト用プロジェクト設定画面キャプチャ03-01
テスト用プロジェクト設定画面キャプチャ03-02
*.objファイル
ライブラリディレクト リパス([構成プロパティ]⇒[リンカー]⇒[追加のライブラリディレクト リ])
テスト用プロジェクト設定画面キャプチャ03-03
テスト用プロジェクト設定画面キャプチャ03-04
ライブラリファイル名([構成プロパティ]⇒[リンカー]⇒[入力])
テスト用プロジェクト設定画面キャプチャ03-05
テスト用プロジェクト設定画面キャプチャ03-06
試験実施
テスト対象のプロジェクトとのリンク設定が完了出来たら、いよいよテスト用のソースコード を実装していきます。
テスト用プロジェクトの「pch.h」に今回のプログラム開発で実装したヘッダーファイルをinclude
します。
テスト用プロジェクトヘッダファイル設定画面キャプチャ
プログラム試験1
試験用テストコード実装
まずはプログラム試験1のテスト用ソースコード の実装です。
2つの機能のテスト用ソースコード を用意します。
メソッド名
内容
TestMethod1
ファイル読み書き用メソッド
TestMethod2
成績ランク算出用メソッド
それぞれのメソッド内でテスト用の構造体を定義します。
テスト用の構造体データを格納した配列をループさせて、設計したテストパターンを機械的 に実施していきます。
* ファイル読み書き機能
typedef struct _testCase {
int TestNo;
char TestFname[BUFSIZE0128];
int TestMode;
int TestExpectedRtn;
char TestExpectedName[BUFSIZE0128];
char TestExpectedGrade[BUFSIZE0128];
} testCase;
typedef struct _testCase {
int TestNo;
int TestNoSub;
char TestGrade[BUFSIZE0128];
int TestExpectedRtn;
char TestExpectedRank;
} testCase;
プログラム試験1のテスト実施フローを以下にまとめます。
テストパターンを設定 テスト対象機能クラスの インスタンス 生成テスト結果のカウントを初期化 (OK/NG) テスト対象の関数実行 戻り値確認 戻り値の結果で分岐 期待通り 期待通りでない テスト結果(戻り値):OK 戻り値が「COM_SUCCESS」? YES NO 配列データの値を確認 配列データの値の確認結果で分岐 期待通り 期待通りでない テスト結果(OK) カウントアップ テスト結果(NG) カウントアップ テスト結果(OK) カウントアップ テスト結果(戻り値):NG テスト結果(NG) カウントアップ テストパターンループ テスト実施結果を出力
フロー図の通り、テスト結果が「OK」となるのは、以下のパターンです。
パターンNo.
戻り値
配列のデータ or 算出した成績ランク
1
期待する値と一致 (COM_SUCCESS)
期待する値と一致
2
期待する値と一致 (COM_FAILED)
---
上記パターンのどちらにも属さない場合は、全てテスト結果「NG」とします。
実際に試験を実施するためのソースコード (ファイル読み書き機能)は以下の通りです。
TEST_METHOD (TestMethod1)
{
typedef struct _testCase {
int TestNo;
char TestFname[BUFSIZE0128];
int TestMode;
int TestExpectedRtn;
char TestExpectedName[BUFSIZE0128];
char TestExpectedGrade[BUFSIZE0128];
} testCase;
int i;
int TestRtn;
int TestArraySize = 0 ;
char TestResultBuf[BUFSIZE1024];
char TestFilePath[BUFSIZE1024];
std ::vector <structGradeRank> testList;
std ::string rootdir = "D: \ aika \\ SVN \\ Project01 \\ x64 \\ Debug" ;
testList.clear ();
int TestOKCount = 0 ;
int TestNGCount = 0 ;
memset ( TestResultBuf, 0x00 , sizeof (TestResultBuf));
testCase testCases[] = {
{1 , {NULL }, COM_MODER, COM_FAILED, {NULL }, {NULL }},
{2 , "WBT01_02.csv" , COM_MODER, COM_FAILED, {NULL }, {NULL }},
{3 , "WBT01_03.csv" , COM_MODER, COM_SUCCESS, {NULL }, {NULL }},
{4 , "WBT01_04.csv" , COM_MODER, COM_SUCCESS, {NULL }, {NULL }},
{5 , "WBT01_05.csv" , COM_MODER, COM_SUCCESS, "テスト5" , "100" },
{6 , "WBT01_06.csv" , COM_MODEW, COM_FAILED, {NULL }, {NULL }},
{7 , "WBT01_07.csv" , COM_MODEW, COM_SUCCESS, {NULL }, {NULL }},
{8 , "WBT01_08.csv" , COM_MODEW, COM_SUCCESS, "テスト8" , "100" }
};
ClassFileReadWrite testClassFileReadWrite;
TestArraySize = sizeof (testCases) / sizeof (testCases[0 ]);
for (i = 0 ; i < TestArraySize; i++)
{
testList.clear ();
memset (TestFilePath, '\0' , sizeof (TestFilePath));
if (testCases[i].TestNo == 8 ) {
structGradeRank data;
memset (&data, 0x00 , sizeof (structGradeRank));
sprintf_s (data.name, testCases[i].TestExpectedName);
sprintf_s (data.grade, testCases[i].TestExpectedGrade);
data.rank = 'A' ;
testList.push_back (data);
}
if (testCases[i].TestFname[0 ] == NULL )
{
TestRtn = testClassFileReadWrite.FileReadWrite (
NULL ,
testCases[i].TestMode,
testList
);
}
else
{
sprintf_s (TestFilePath, " %s\\%s " , rootdir.c_str (), testCases[i].TestFname);
TestRtn = testClassFileReadWrite.FileReadWrite (
TestFilePath,
testCases[i].TestMode,
testList
);
}
memset (TestResultBuf, 0x00 , sizeof (TestResultBuf));
if (TestRtn == testCases[i].TestExpectedRtn) {
sprintf_s (TestResultBuf, "テスト %02d :OK[TestRtn: %d Expected: %d ] \n " , testCases[i].TestNo, TestRtn, testCases[i].TestExpectedRtn);
Logger::WriteMessage (TestResultBuf);
if (TestRtn == COM_SUCCESS) {
if ((testCases[i].TestMode == COM_MODER) && (!testList.empty ()))
{
if ((strcmp (testCases[i].TestExpectedName, testList.at (0 ).name) == 0 ) &&
(strcmp (testCases[i].TestExpectedGrade, testList.at (0 ).grade) == 0 )) {
sprintf_s (TestResultBuf, "テスト %02d :OK[Name(Expected): %s Name(Actual): %s ] \n " ,
testCases[i].TestNo,
testCases[i].TestExpectedName,
testList.at (0 ).name
);
Logger::WriteMessage (TestResultBuf);
sprintf_s (TestResultBuf, "テスト %02d :OK[Grade(Expected): %s Grade(Actual): %s ] \n " ,
testCases[i].TestNo,
testCases[i].TestExpectedGrade,
testList.at (0 ).grade
);
Logger::WriteMessage (TestResultBuf);
TestOKCount++;
}
else {
sprintf_s (TestResultBuf, "テスト %02d :NG[Name(Expected): %s Name(Actual): %s ] \n " ,
testCases[i].TestNo,
testCases[i].TestExpectedName,
testList.at (0 ).name
);
Logger::WriteMessage (TestResultBuf);
sprintf_s (TestResultBuf, "テスト %02d :NG[Grade(Expected): %s Grade(Actual): %s ] \n " ,
testCases[i].TestNo,
testCases[i].TestExpectedGrade,
testList.at (0 ).grade
);
Logger::WriteMessage (TestResultBuf);
TestNGCount++;
}
}
else
{
sprintf_s (TestResultBuf, "テスト %02d :OK[Mode: %d size: %d ] \n " ,
testCases[i].TestNo,
testCases[i].TestMode,
(int )testList.size ()
);
Logger::WriteMessage (TestResultBuf);
TestOKCount++;
}
}
else {
TestOKCount++;
}
}
else {
sprintf_s (TestResultBuf, "テスト %02d :NG[TestRtn: %d Expected: %d ] \n " , testCases[i].TestNo, TestRtn, testCases[i].TestExpectedRtn);
Logger::WriteMessage (TestResultBuf);
TestNGCount++;
}
}
sprintf_s ( TestResultBuf, "テスト結果[OK: %3d 件 NG: %3d 件] \n " , TestOKCount, TestNGCount);
Logger::WriteMessage ( TestResultBuf );
}
テスト用ソースコード の内部の処理は以下の順で行います。
テスト用構造体の配列データを用意
テストケースごとに試験用の入力パラメータを用意
配列データをループさせ、テストケースごとに試験対象の機能を実行
戻り値とデータを確認
上記の処理 4. でテストケースごとの実施結果をLogger::WriteMessage関数 で出力ウィンドウに表示しています。また、OK、NGそれぞれの件数を集計し、全てのテストケース実施後、その集計結果も出力ウィンドウに表示させます。
成績ランク算出機能も同様のソースコード を実装していきます。
TEST_METHOD (TestMethod2)
{
typedef struct _testCase {
int TestNo;
int TestNoSub;
char TestGrade[BUFSIZE0128];
int TestExpectedRtn;
char TestExpectedRank;
} testCase;
int TestRtn;
std ::vector <structGradeRank> testList;
char TestResultBuf[BUFSIZE1024];
char TestCaseName[BUFSIZE0128];
int TestOKCount = 0 ;
int TestNGCount = 0 ;
ClassCalcGradeRank testClassCalcGradeRank;
testCase testCases[] = {
{ 1 , 0 , "9999" , COM_FAILED, '\0' },
{ 2 , 0 , "95" , COM_SUCCESS, 'A' },
{ 3 , 0 , "85" , COM_SUCCESS, 'B' },
{ 4 , 0 , "70" , COM_SUCCESS, 'C' },
{ 5 , 0 , "30" , COM_SUCCESS, 'D' },
{ 6 , 1 , "101" , COM_SUCCESS, 'E' },
{ 6 , 2 , "-1" , COM_SUCCESS, 'E' },
{ 7 , 1 , "100.04" , COM_SUCCESS, 'A' },
{ 7 , 2 , "89.95" , COM_SUCCESS, 'A' },
{ 8 , 1 , "89.94" , COM_SUCCESS, 'B' },
{ 8 , 2 , "74.95" , COM_SUCCESS, 'B' },
{ 9 , 1 , "74.94" , COM_SUCCESS, 'C' },
{ 9 , 2 , "59.95" , COM_SUCCESS, 'C' },
{ 10 , 1 , "59.94" , COM_SUCCESS, 'D' },
{ 10 , 2 , "-0.04" , COM_SUCCESS, 'D' },
{ 11 , 1 , "100.05" , COM_SUCCESS, 'E' },
{ 11 , 2 , "-0.05" , COM_SUCCESS, 'E' },
{ 12 , 1 , "P" , COM_SUCCESS, 'E' },
{ 12 , 2 , "#" , COM_SUCCESS, 'E' },
{ 12 , 3 , "ア" , COM_SUCCESS, 'E' },
{ 12 , 4 , "P" , COM_SUCCESS, 'E' },
{ 12 , 5 , "@" , COM_SUCCESS, 'E' },
{ 12 , 6 , "あ" , COM_SUCCESS, 'E' },
{ 12 , 7 , "ア" , COM_SUCCESS, 'E' },
{ 12 , 8 , "百" , COM_SUCCESS, 'E' },
};
structGradeRank TestData;
for (int i = 0 ; i < sizeof (testCases) / sizeof (testCase); i++)
{
memset ( &TestData, 0x00 , sizeof (TestData) );
memset (TestCaseName, 0x00 , sizeof (TestCaseName));
testList.clear ();
if ( testCases[i].TestNoSub == 0 ) {
sprintf_s (TestData.name, "TestName %02d " , testCases[i].TestNo);
sprintf_s (TestCaseName, "テスト %02d " , testCases[i].TestNo);
}
else {
sprintf_s (TestData.name, "TestName %02d _ %02d " , testCases[i].TestNo, testCases[i].TestNoSub);
sprintf_s (TestCaseName, "テスト %02d - %02d " , testCases[i].TestNo, testCases[i].TestNoSub);
}
if ( testCases[i].TestNo > 1 )
{
strcpy_s ( TestData.grade, testCases[i].TestGrade );
testList.push_back (TestData);
}
TestRtn = testClassCalcGradeRank.CalcGradeRank (
testList
);
memset (TestResultBuf, 0x00 , sizeof (TestResultBuf));
if ( TestRtn == testCases[i].TestExpectedRtn ) {
sprintf_s (TestResultBuf, " %s :OK[TestRtn: % d Expected : % d ] \n " , TestCaseName, TestRtn, testCases[i].TestExpectedRtn);
Logger::WriteMessage (TestResultBuf);
if ( TestRtn == COM_SUCCESS ) {
if ( (testCases[i].TestExpectedRank & testList.at (0 ).rank ) == testCases[i].TestExpectedRank )
{
sprintf_s (TestResultBuf, " %s :OK[Rank(Expected):0x %02X Rank(Actual):0x %02X ] \n " ,
TestCaseName,
testCases[i].TestExpectedRank,
testList.at (0 ).rank
);
Logger::WriteMessage (TestResultBuf);
TestOKCount++;
}
else
{
sprintf_s (TestResultBuf, " %s :NG[Rank(Expected):0x %02X Rank(Actual):0x %02X ] \n " ,
TestCaseName,
testCases[i].TestExpectedRank,
testList.at (0 ).rank
);
Logger::WriteMessage (TestResultBuf);
TestNGCount++;
}
}
else {
TestOKCount++;
}
}
else {
sprintf_s (TestResultBuf, " %s :NG[TestRtn: %d Expected: %d ] \n " , TestCaseName, TestRtn, testCases[i].TestExpectedRtn);
Logger::WriteMessage (TestResultBuf);
TestNGCount++;
}
}
sprintf_s (TestResultBuf, "テスト結果[OK: %3d 件 NG: %3d 件] \n " , TestOKCount, TestNGCount);
Logger::WriteMessage (TestResultBuf);
}
試験実施
実施手順
ファイル読み書き機能、成績ランク算出機能それぞれでテストを行います。
テストの実行方法は以下の通りです。
Visual Studio で[テスト]→[テストエクスプローラ ー]を選択し、ダイアログを開きます。
テスト用プロジェクト実施画面キャプチャ01
そのダイアログ内の左端の2つのボタンのいずれかを選択し、テストを実行します。
テスト用プロジェクト実施画面キャプチャ02
実施結果
上記の手順で実施した結果は以下の通りです。
テスト用プロジェクト実践結果画面キャプチャ01
テスト用プロジェクト実践結果画面キャプチャ02
試験成績記入
試験実施後、出力ウィンドウに表示された結果を確認して以下の様に試験結果を記入します。
テスト用プロジェクト成績記入画面キャプチャ01
テスト用プロジェクト成績記入画面キャプチャ02
障害起票
今回試験を実施してNGを2件確認しました。NGが出た場合は、その内容をドキュメントに記録しておきます。
大抵の場合はエクセル形式で障害管理帳が用意されています。
現場によってはRedmine で障害を管理している所もあります。
どんな形式にせよ、障害を記録する時は他の人が自分と同じ手順で試験を実施した時、
同じ障害が再現できるように書くことです。
これは障害対応後、同じ手順を踏んで障害が改善されたのを確認するためにも必要です。
記録する時は以下の項目を書くことが多いです。
何を(障害を起こしたもの)
どうした(障害を起こした手順)
どうなった(障害の内容)
本来どうあるべき(障害が起きない時の内容)
今回の場合だと、以下の様に書きます。
ファイル読み書き機能
ファイル読み書き機能
以下の手順を実施
結果ファイル(半角カンマ「,」無)をプログラムと同じ場所に格納
引数に結果ファイル名を指定してプログラムを実行
配列に設定したデータ(名前)が期待していたものと異なる
配列の「name」メンバ変数に行データが設定される
配列には何も設定されない
成績ランク算出機能
成績ランク算出機能
以下の手順を実施
試験の点数を「100.05」で配列に設定して、プログラムを実行
算出した成績ランクが期待していたものと異なる
「E」が算出される
障害調査
障害の記録作業が完了したら、その障害の原因追究をしていきます。
他の人の障害調査を依頼された場合、まずは自分も同じ障害を再現出来るか確認します。
そこから開発ツールを使い、デバッグ &ステップ実行をしながら障害の原因箇所を見つけていきます。
今回の障害は以下の原因が考えられます。
ファイル読み書き機能の障害
半角カンマ「,」が無い行データで半角カンマの検出をする時、検出無しにもかかわらず、配列にデータを格納していた
成績ランク算出機能の障害
障害対応
機能
対応内容
ファイル読み書き機能
最初の検出で失敗した行データはスキップ
成績ランク算出
double型からstringに変換する時に絶対値0.001を足す
ソースコード の変更箇所は以下の通りです。
テスト用プロジェクト障害対応画面キャプチャ01
* 成績ランク算出機能
テスト用プロジェクト障害対応画面キャプチャ02
上記の対応を追加したソースコード をビルドし、再度プログラム試験1を実施します。
* ファイル読み書き機能
テスト用プロジェクト障害対合後実施結果画面キャプチャ01
* 成績ランク算出機能
テスト用プロジェクト障害対合後実施結果画面キャプチャ02
障害対応前は2件の「NG」が出ていましたが、障害対応後は「NG」→「OK」に変わっています。
これでプログラム試験1の試験項目は全てパスしたことになりました。
プログラム試験2
プログラム試験1をパスしたので、次はプログラム試験2です。
プログラム試験2ではユーザーの要求通りのプログラムになっているかを確認します。
そのため、ユーザーと同じ条件でプログラムを実行していきます。
試験仕様書に記載した手順に従ってプログラムを実行した結果は以下の通りです。
プログラム試験2実施結果画面キャプチャ
それぞれの試験パターンでの結果ファイルの中身は以下の様になりました。
プログラム試験2(ケースNo.2)実施結果画面キャプチャ
* ケースNo.3(読み取り専用を解除)
プログラム試験2(ケースNo.3)実施結果画面キャプチャ
* ケースNo.4
プログラム試験2(ケースNo.4)実施結果画面キャプチャ
* ケースNo.5
プログラム試験2(ケースNo.5)実施結果画面キャプチャ
いずれも試験仕様書の「期待される出力結果」欄の通りになりました。
プログラム試験2では「NG」は出なかったです。
これでプログラム試験1、2の試験項目が全て「OK」になったので、
ユーザーの要求に応じた成績算出プログラムが完成しました。
最後に
ここまで長々と記事を書いてきました。
当初は1つの記事に収めるつもりでしたが、書きたいことがどんどん増えて気付けば4回も連載する形になりました。
元々このテーマを選んだのは、テスト作業に不慣れな人が少しでも参考になればと思っての事でした。
外の現場ではテスト作業から始めることが多く、その試験項目の設計作業を任されることも多々あるので、このシリーズの記事が役に立ってもらえれば幸いです。
大事なのは「ストーリー 」が見えるようにすること。
設計~コーディング~試験の紐づきが自分以外の人にも見えるように試験項目を作るのがベストです。
そのために以下の点を意識するようにしてください。
何が変わるのか(対応前後の動作等)
着目点を整理する(正常系/異常系の時の動作等)
上記2点を押さえておけば、方向性の定まった試験項目を設計できるようになります。