次の方法で共有


C++ DLL 用の単体テストの記述

このチュートリアルでは、テスト ファースト メソドロジを使ってネイティブ C++ DLL を開発する方法について説明します。 基本手順は次のとおりです。

  1. ネイティブのテスト プロジェクトを作成する。 テスト プロジェクトは、DLL プロジェクトと同じソリューションに格納されます。

  2. DLL プロジェクトを作成する。 このチュートリアルでは新しい DLL を作成しますが、既存の DLL をテストする手順は類似しています。

  3. DLL 関数をテストに表示させる

  4. テストを反復拡張する。 コードの開発がテストによって主導される「red-green-refactor」サイクルの使用をお勧めします。

  5. 失敗したテストをデバッグする。 テストをデバッグ モードで実行することができます。

  6. テストを変更しないままリファクタリングする。 リファクタリングとは、外部の動作を変更せずに、コードの構造を改善するという意味です。 リファクタリングを行うと、コードのパフォーマンス、拡張性、または読みやすさが向上します。 目的は動作を変更しないことであるため、コードへの変更をリファクタリングするときはテストを変更しないでください。 テストは、リファクタリング中にバグを持ち込んでいないことを確認するのに役立ちます。

  7. カバレッジを確認する。 単体テストは、コードをより多く実行する場合はさらに有用です。 コードのどの部分がテストで使用されたかを見つけ出すことができます。

  8. 外部リソースから単位を分離する。 一般に、DLL は、他の DLL、データベース、またはリモートのサブシステムなど、開発中のシステムの他のコンポーネントに依存しています。 各単位をその依存関係から分離してテストすると役立ちます。 外部コンポーネントは、テストの実行を遅くする可能性があります。 開発中、他のコンポーネントが不完全であることもあり得ます。

ネイティブ単体テスト プロジェクトを作成する

  1. [ファイル] メニューで、 [新規]>[プロジェクト]をクリックします。

    Visual Studio 2017 以前:[インストール済み][テンプレート][Visual C++][テスト] の順に展開します。 Visual Studio 2019:[言語] を C++ に設定し、検索ボックスに "test" と入力します。

    ネイティブ単体テスト プロジェクト テンプレートまたはインストールされている他の好みのフレームワークを選びます。 Google Test や Boost.Test などの別のテンプレートを選んだ場合、基本的な原則は同じですが、いくつかの詳細は異なります。

    このチュートリアルでは、テスト プロジェクトの名前は NativeRooterTestです。

  2. 新しいプロジェクトで、 unittest1.cppを検査します。

    Test project with TEST_CLASS and TEST_METHOD

    次のことに注意してください。

    • 各テストは TEST_METHOD(YourTestName){...} を使用して定義されます。

      従来の関数の署名を記述する必要はありません。 署名は、マクロ TEST_METHOD によって作成されます。 マクロは、void を返すインスタンス関数を生成します。 また、テスト メソッドに関する情報を返す静的関数も生成します。 この情報により、テスト エクスプ ローラーはメソッドを見つけます。

    • テスト メソッドは、 TEST_CLASS(YourClassName){...}を使用してクラスにグループ化されます。

      テストが実行されると、各テスト クラスのインスタンスが作成されます。 テスト メソッドが呼び出される順序は決まっていません。 各モジュール、クラス、またはメソッドの前後に呼び出される特殊なメソッドを定義することができます。

  3. テストがテスト エクスプ ローラーで実行することを確認します。

    1. 幾らかのテスト コードを挿入します。

      TEST_METHOD(TestMethod1)
      {
          Assert::AreEqual(1,1);
      }
      

      Assert クラスは、テスト メソッドで結果を確認するために使用するいくつかの静的メソッドを提供することに注意してください。

    2. [テスト] メニューで [実行]>[すべてのテスト] を選びます。

      テストがビルドし、実行します。

      テスト エクスプローラーが表示されます。

      [合格したテスト]の下にテストが表示されます。

      Unit Test Explorer with one passed test

DLL プロジェクトを作成する

次の手順は、Visual Studio 2019 で DLL プロジェクトを作成する方法を示しています。

  1. Windows デスクトップ ウィザードを使用し、C++ プロジェクトを作成します。ソリューション エクスプローラーでソリューションを右クリックし、[追加][新しいプロジェクト] の順に選択します。 [言語] を C++ に設定し、検索ボックスに "windows" と入力します。 結果の一覧から [Windows デスクトップ ウィザード] を選択します。

    このチュートリアルでは、プロジェクトの名前を RootFinderとします。

  2. 作成をクリックします。 次のダイアログの [アプリケーションの種類][ダイナミックリンク ライブラリ (dll)] を選択し、[シンボルのエクスポート] にもチェックを入れます。

    [シンボルのエクスポート] オプションは、エクスポートされたメソッドの宣言に使用できる便利なマクロを生成します。

    C++ project wizard set for DLL and Export Symbols

  3. プリンシパル .h ファイルでエクスポートされた関数を宣言します。

    New DLL code project and .h file with API macros

    宣言子 __declspec(dllexport) は、クラスのパブリック メンバーと保護されるメンバーが DLL の外部で表示できるようにします。 詳細については、「 Using dllimport and dllexport in C++ Classes」を参照してください。

  4. プリンシパル .cpp ファイルでは、最小限の本体を関数に追加します。

        // Find the square root of a number.
        double CRootFinder::SquareRoot(double v)
        {
            return 0.0;
        }
    

DLL プロジェクトにテスト プロジェクトを結合する

  1. DLL プロジェクトをテスト プロジェクトのプロジェクト参照に追加します。

    1. ソリューション エクスプローラーでプロジェクト ノードを右クリックして、[追加]>[参照] を選びます。

    2. [参照の追加] ダイアログ ボックスで、DLL プロジェクトを選択し、 [追加]をクリックします。

      C++ project properties | Add New Reference

  2. プリンシパルの単体テストの .cpp ファイルに、DLL コードの .h ファイルを含めます。

    #include "..\RootFinder\RootFinder.h"
    
  3. エクスポートされた関数を使用する基本テストを追加します。

    TEST_METHOD(BasicTest)
    {
       CRootFinder rooter;
       Assert::AreEqual(
          // Expected value:
          0.0,
          // Actual value:
          rooter.SquareRoot(0.0),
          // Tolerance:
          0.01,
         // Message:
         L"Basic test failed",
         // Line number - used if there is no PDB file:
         LINE_INFO());
    }
    
  4. ソリューションをビルドします。

    テスト エクスプローラーに新しいテストが表示されます。

  5. テスト エクスプローラー[すべて実行] をクリックします。

    Unit Test Explorer - Basic Test passed

    テストとコード プロジェクトをセット アップして、コード プロジェクトで関数を実行するテストを実行できることを確認しました。 ここで、実際のテストおよびコードの記述を開始できます。

テストを繰り返し増やして成功させる

  1. 新しいテストを追加します。

    TEST_METHOD(RangeTest)
    {
      CRootFinder rooter;
      for (double v = 1e-6; v < 1e6; v = v * 3.2)
      {
        double actual = rooter.SquareRoot(v*v);
        Assert::AreEqual(v, actual, v/1000);
      }
    }
    

    ヒント

    合格したテスト内容を変更しないことをお勧めします。 代わりに、新しいテストを追加し、テストが合格するようにコードを更新してから別のテストを追加する、という過程を繰り返します。

    ユーザーが要件を変更したら、正しくなくなったテストを無効にします。 新しいテストを作成し、一度に 1 つずつ、同じ増分方式で処理するようにします。

  2. ソリューションをビルドし、テスト エクスプローラー[すべて実行] を選択します。

    新しいテストは失敗します。

    The RangeTest fails

    ヒント

    各テストが記述した後すぐに失敗することを確認します。 これは、絶対に失敗しないテストを記述するという簡単なミスを避けることに役立ちます。

  3. 新しいテストが成功するように、DLL のコードを修正します。

    #include <math.h>
    ...
    double CRootFinder::SquareRoot(double v)
    {
      double result = v;
      double diff = v;
      while (diff > result/1000)
      {
        double oldResult = result;
        result = result - (result*result - v)/(2*result);
        diff = abs (oldResult - result);
      }
      return result;
    }
    
  4. ソリューションをビルドし、テスト エクスプローラー[すべて実行] を選択します。

    両方のテストが合格します。

    Unit Test Explorer - Range Test passed

    ヒント

    一度に 1 つのテストを追加してコードを開発します。 各反復処理の後にすべてのテストが合格することを確認します。

失敗したテストをデバッグする

  1. 別のテストを追加します。

    #include <stdexcept>
    ...
    // Verify that negative inputs throw an exception.
    TEST_METHOD(NegativeRangeTest)
    {
      wchar_t message[200];
      CRootFinder rooter;
      for (double v = -0.1; v > -3.0; v = v - 0.5)
      {
        try
        {
          // Should raise an exception:
          double result = rooter.SquareRoot(v);
    
          _swprintf(message, L"No exception for input %g", v);
          Assert::Fail(message, LINE_INFO());
        }
        catch (std::out_of_range ex)
        {
          continue; // Correct exception.
        }
        catch (...)
        {
          _swprintf(message, L"Incorrect exception for %g", v);
          Assert::Fail(message, LINE_INFO());
        }
      }
    }
    
  2. ソリューションをビルドし、 [すべて実行]をクリックします。

  3. 失敗したテストを開きます (またはダブルクリックします)。

    失敗したアサーションが強調表示されます。 エラー メッセージは、テスト エクスプローラーの [詳細] ウィンドウに表示されます。

    NegativeRangeTests failed

  4. テストが失敗した理由を表示するには、関数をステップ実行します。

    1. SquareRoot 関数の始めに、ブレークポイントを設定します。

    2. 失敗したテストのショートカット メニューで [選択したテストのデバッグ]をクリックします。

      実行がブレークポイントで停止したら、コードをステップ実行します。

  5. 開発中の関数にコードを挿入します。

    
    #include <stdexcept>
    ...
    double CRootFinder::SquareRoot(double v)
    {
        // Validate parameter:
        if (v < 0.0)
        {
          throw std::out_of_range("Can't do square roots of negatives");
        }
    
    
  6. 今回は、すべてのテストに合格します。

    All tests pass

ヒント

個々のテストに実行順序を定める依存関係がない場合、ツール バーの設定メニューで並列テストの実行を有効にします。 これにより、すべてのテスト実行にかかる時間を著しく短縮できます。

テストを変更せずにコードをリファクタリングする

  1. SquareRoot 関数の中心的な計算を簡素化します。

    // old code:
    //   result = result - (result*result - v)/(2*result);
    // new code:
         result = (result + v/result)/2.0;
    
    
  2. ソリューションをビルドし、 [すべて実行]をクリックして、エラーが持ち込まれていないことを確認します。

    ヒント

    一連の単体テストがうまくいくことは、コードを変更する際にバグが持ち込まれていないという信用を与えます。

    常に他の変更とは別にリファクタリングしてください。

次のステップ

  • 分離。 ほとんどの DLL は、データベースや他の DLL など、他のサブシステムに依存しています。 これらの他のコンポーネントは、多くの場合、並列で開発されます。 他のコンポーネントがまだ使用可能でないときに単体テストを行えるようにするには、モックまたは

  • ビルド確認テスト。 設定された間隔で、チームのビルド サーバーでテストを実行することができます。 これにより、複数のチーム メンバーの作業が統合されてもバグが持ち込まれることはありません。

  • チェックイン テスト。 各チーム メンバーがコードをソース管理にチェックインする前にテストを実行することを要求できます。 通常、これはビルド確認テストの完全なセットのサブセットです。

    また、最低限のコード カバレッジも要求できます。