大化通信

大化物流開発合同会社の社員から技術発信をしていきます

成績ランク算出プログラムを設計・製造してみた(プログラム試験実践編)

はじめに

どうも、迷走星人です。 4回目はプログラム試験の実践部分の話を出来たらと思います。 尚、この段階では以下の工程が完了しているものとします。

  • プログラムの設計&レビュー
  • プログラムの実装&レビュー
  • プログラム試験の設計&レビュー

試験環境準備

単体テスト

プログラム試験1の試験を実施するために、テスト用のプロジェクトを用意します。 テスト用のプロジェクトは、Visual Studioの「ネイティブ単体テストプロジェクト」で作成します。

テスト用プロジェクトの作成

まずはテスト用のプロジェクト作成をしていきます。作成手順は以下に書いていきます。

  1. プログラムを作成したソリューションから[追加]->[新しいプロジェクト]を選択します。

テスト用プロジェクト作成画面キャプチャ01
2. プロジェクトウィザードから「ネイティブ単体テストプロジェクト」を選択します。

テスト用プロジェクト作成画面キャプチャ02
3. 自分のPCの任意の場所にプロジェクトを作成します。ここでは「TaikaProj01_CalcGradeRankTest」というプロジェクト名にします。

テスト用プロジェクト作成画面キャプチャ03
4. 単体テスト用のプロジェクトが作成されました。このテスト用ソースコードにテスト用の実装を進めていきます。

テスト用プロジェクト作成画面キャプチャ04

テスト用プロジェクトの設定

テスト用のプロジェクトが作成出来たら、テスト対象のプロジェクトとリンクするように設定します。

  1. テスト用プロジェクトで、[参照]->[参照の追加]を選択します。

テスト用プロジェクト設定画面キャプチャ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;                         // テストNo.
    char    TestFname[BUFSIZE0128];         // テスト用結果ファイル名
    int     TestMode;                       // テスト用モード
    int     TestExpectedRtn;                // 期待する戻り値
    char    TestExpectedName[BUFSIZE0128];  // 期待する名前
    char    TestExpectedGrade[BUFSIZE0128]; // 期待する点数
} testCase;
  • 成績ランク算出機能
typedef struct _testCase {
    int     TestNo;                 // テストNo.
    int     TestNoSub;              // テストサブNo.
    char    TestGrade[BUFSIZE0128]; // テスト用試験点数
    int     TestExpectedRtn;        // 期待する戻り値
    char    TestExpectedRank;       // 期待するランク
} testCase;

プログラム試験1のテスト実施フローを以下にまとめます。

テストパターンを設定テスト対象機能クラスのインスタンス生成テスト結果のカウントを初期化(OK/NG)テスト対象の関数実行戻り値確認戻り値の結果で分岐期待通り期待通りでないテスト結果(戻り値):OK戻り値が「COM_SUCCESS」?YESNO配列データの値を確認配列データの値の確認結果で分岐期待通り期待通りでないテスト結果(OK)カウントアップテスト結果(NG)カウントアップテスト結果(OK)カウントアップテスト結果(戻り値):NGテスト結果(NG)カウントアップテストパターンループテスト実施結果を出力

フロー図の通り、テスト結果が「OK」となるのは、以下のパターンです。

パターンNo. 戻り値 配列のデータ or 算出した成績ランク
1 期待する値と一致
(COM_SUCCESS)
期待する値と一致
2 期待する値と一致
(COM_FAILED)
---

上記パターンのどちらにも属さない場合は、全てテスト結果「NG」とします。

実際に試験を実施するためのソースコード(ファイル読み書き機能)は以下の通りです。

/// <summary>
/// ファイル読み書き機能のテスト用メソッド
/// </summary>
TEST_METHOD(TestMethod1)
{
    // テスト用構造体データ
    typedef struct _testCase {
        int     TestNo;                         // テストNo.
        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 {
                // 戻り値が「COM_FAILED」
                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 );
}

テスト用ソースコードの内部の処理は以下の順で行います。

  1. テスト用構造体の配列データを用意
  2. テストケースごとに試験用の入力パラメータを用意
  3. 配列データをループさせ、テストケースごとに試験対象の機能を実行
  4. 戻り値とデータを確認

上記の処理 4. でテストケースごとの実施結果をLogger::WriteMessage関数で出力ウィンドウに表示しています。また、OK、NGそれぞれの件数を集計し、全てのテストケース実施後、その集計結果も出力ウィンドウに表示させます。 成績ランク算出機能も同様のソースコードを実装していきます。

/// <summary>
/// 成績ランク算出機能のテスト用メソッド
/// </summary>
TEST_METHOD(TestMethod2)
{
    // テスト用構造体データ
    typedef struct _testCase {
        int     TestNo;                 // テストNo.
        int     TestNoSub;              // テストサブNo.
        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 ) {
                // 戻り値が「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 {
                // 戻り値が「COM_FAILED」
                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);
}
試験実施
実施手順

ファイル読み書き機能、成績ランク算出機能それぞれでテストを行います。 テストの実行方法は以下の通りです。

  1. Visual Studioで[テスト]→[テストエクスプローラー]を選択し、ダイアログを開きます。

テスト用プロジェクト実施画面キャプチャ01

  1. そのダイアログ内の左端の2つのボタンのいずれかを選択し、テストを実行します。

テスト用プロジェクト実施画面キャプチャ02

実施結果

上記の手順で実施した結果は以下の通りです。

  • ファイル読み書き機能

テスト用プロジェクト実践結果画面キャプチャ01

  • 成績ランク算出機能

テスト用プロジェクト実践結果画面キャプチャ02

試験成績記入

試験実施後、出力ウィンドウに表示された結果を確認して以下の様に試験結果を記入します。

テスト用プロジェクト成績記入画面キャプチャ01

テスト用プロジェクト成績記入画面キャプチャ02

障害起票

今回試験を実施してNGを2件確認しました。NGが出た場合は、その内容をドキュメントに記録しておきます。 大抵の場合はエクセル形式で障害管理帳が用意されています。 現場によってはRedmineで障害を管理している所もあります。 どんな形式にせよ、障害を記録する時は他の人が自分と同じ手順で試験を実施した時、 同じ障害が再現できるように書くことです。 これは障害対応後、同じ手順を踏んで障害が改善されたのを確認するためにも必要です。 記録する時は以下の項目を書くことが多いです。

  1. 何を(障害を起こしたもの)
  2. どうした(障害を起こした手順)
  3. どうなった(障害の内容)
  4. 本来どうあるべき(障害が起きない時の内容)

今回の場合だと、以下の様に書きます。

  • ファイル読み書き機能

    1. ファイル読み書き機能
    2. 以下の手順を実施
      1. 結果ファイル(半角カンマ「,」無)をプログラムと同じ場所に格納
      2. 引数に結果ファイル名を指定してプログラムを実行
    3. 配列に設定したデータ(名前)が期待していたものと異なる
      • 配列の「name」メンバ変数に行データが設定される
    4. 配列には何も設定されない
  • 成績ランク算出機能

    1. 成績ランク算出機能
    2. 以下の手順を実施
      1. 試験の点数を「100.05」で配列に設定して、プログラムを実行
    3. 算出した成績ランクが期待していたものと異なる
      • 「E」と算出したいが、「A」が算出される
    4. 「E」が算出される
障害調査

障害の記録作業が完了したら、その障害の原因追究をしていきます。 他の人の障害調査を依頼された場合、まずは自分も同じ障害を再現出来るか確認します。 そこから開発ツールを使い、デバッグ&ステップ実行をしながら障害の原因箇所を見つけていきます。 今回の障害は以下の原因が考えられます。

  • ファイル読み書き機能の障害
    • 半角カンマ「,」が無い行データで半角カンマの検出をする時、検出無しにもかかわらず、配列にデータを格納していた
  • 成績ランク算出機能の障害
障害対応
機能 対応内容
ファイル読み書き機能 最初の検出で失敗した行データはスキップ
成績ランク算出 double型からstringに変換する時に絶対値0.001を足す

ソースコードの変更箇所は以下の通りです。

  • ファイル読み書き機能

テスト用プロジェクト障害対応画面キャプチャ01
* 成績ランク算出機能

テスト用プロジェクト障害対応画面キャプチャ02

上記の対応を追加したソースコードをビルドし、再度プログラム試験1を実施します。 * ファイル読み書き機能

テスト用プロジェクト障害対合後実施結果画面キャプチャ01
* 成績ランク算出機能

テスト用プロジェクト障害対合後実施結果画面キャプチャ02

障害対応前は2件の「NG」が出ていましたが、障害対応後は「NG」→「OK」に変わっています。 これでプログラム試験1の試験項目は全てパスしたことになりました。

プログラム試験2

プログラム試験1をパスしたので、次はプログラム試験2です。 プログラム試験2ではユーザーの要求通りのプログラムになっているかを確認します。 そのため、ユーザーと同じ条件でプログラムを実行していきます。 試験仕様書に記載した手順に従ってプログラムを実行した結果は以下の通りです。

プログラム試験2実施結果画面キャプチャ

それぞれの試験パターンでの結果ファイルの中身は以下の様になりました。

  • ケースNo.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点を押さえておけば、方向性の定まった試験項目を設計できるようになります。