演習 - 単体テストをアプリケーションに追加する

完了

このユニットでは、Microsoft Azure Pipelines で作成した自動ビルドに単体テストを追加します。 回帰バグがチームのコードに入り込み、ランキングのフィルター処理機能を中断させています。 具体的には、正しくないゲーム モードが常に表示されます。

次の図は、この問題を示しています。 あるユーザーが "Milky Way" を選択してそのゲーム マップのスコアだけを表示しようとすると、Andromeda などの他のゲーム マップの結果も表示されます。

不正な結果を示すランキングのスクリーンショット。Andromeda Galaxy のスコアが、Milky Way Galaxy の一覧に表示されています。

チームでは、テスターに到達する前にエラーを見つけたいと考えています。 単体テストは、自動的に回帰バグをテストする優れた方法です。

プロセスのこの時点で単体テストを追加することで、チームは Space Game Web アプリを速やかに改善できるようになります。 アプリケーションではドキュメント データベースを使用して、ハイ スコアとプレイヤーのプロファイルを格納します。 ここでは、ローカル テスト データを使用します。 後で、アプリをライブ データベースに接続する予定です。

C# アプリケーションで使用可能な単体テスト フレームワークは数多くあります。 ここでは、コミュニティに人気がある NUnit を使用します。

使用する単体テストを次に示します。

[TestCase("Milky Way")]
[TestCase("Andromeda")]
[TestCase("Pinwheel")]
[TestCase("NGC 1300")]
[TestCase("Messier 82")]
public void FetchOnlyRequestedGameRegion(string gameRegion)
{
    const int PAGE = 0; // take the first page of results
    const int MAX_RESULTS = 10; // sample up to 10 results

    // Form the query predicate.
    // This expression selects all scores for the provided game region.
    Expression<Func<Score, bool>> queryPredicate = score => (score.GameRegion == gameRegion);

    // Fetch the scores.
    Task<IEnumerable<Score>> scoresTask = _scoreRepository.GetItemsAsync(
        queryPredicate, // the predicate defined above
        score => 1, // we don't care about the order
        PAGE,
        MAX_RESULTS
    );
    IEnumerable<Score> scores = scoresTask.Result;

    // Verify that each score's game region matches the provided game region.
    Assert.That(scores, Is.All.Matches<Score>(score => score.GameRegion == gameRegion));
}

ゲームの種類とゲーム マップの任意の組み合わせで、ランキングをフィルター処理することができます。

このテストではランキングでハイ スコアを照会し、各結果が指定されたゲーム マップと一致していることを確認します。

NUnit テストメソッドでは、TestCase で、そのメソッドをテストするために使用するインライン データが提供されます。 ここで、NUnit は次のような FetchOnlyRequestedGameRegion 単体テスト メソッドを呼び出します。

FetchOnlyRequestedGameRegion("Milky Way");
FetchOnlyRequestedGameRegion("Andromeda");
FetchOnlyRequestedGameRegion("Pinwheel");
FetchOnlyRequestedGameRegion("NGC 1300");
FetchOnlyRequestedGameRegion("Messier 82");

テストの最後に Assert.That メソッドが呼び出されることに注目してください。 "アサーション" は、true であると宣言する条件またはステートメントです。 条件が false になった場合、コードにバグがあることを示します。 NUnit では、指定されたインライン データを使用して各テスト メソッドを実行し、成功または失敗したテストとして結果を報告します。

多くの単体テスト フレームワークでは、自然言語のような確認方法が提供されます。 これらの方法は、テストを読み取りやすくしたり、アプリケーションの要件にテストをマップしやすくしたりするために役立ちます。

この例で行われたアサーションについて考えてみます。

Assert.That(scores, Is.All.Matches<Score>(score => score.GameRegion == gameRegion));

この行は次のように読み取れます。

返された各スコアのゲーム リージョンが、指定されたゲーム リージョンと一致していることをアサートする。

実行するプロセスを以下に示します。

  1. GitHub リポジトリから単体テストを含むブランチをフェッチします。
  2. ローカルでテストを実行し、それらが成功したことを確認します。
  3. パイプライン構成にタスクを追加し、テストを実行して結果を収集します。
  4. GitHub リポジトリにブランチをプッシュします。
  5. Azure Pipelines プロジェクトで自動的にアプリケーションがビルドされ、テストが実行されることを監視します。

GitHub からブランチをフェッチする

ここでは、GitHub から unit-tests ブランチをフェッチしてチェックアウトし (つまり、そのブランチに切り替え) ます。

このブランチには、前のモジュールで作業した Space Game プロジェクトと、最初に使用する Azure Pipelines 構成が含まれています。

  1. Visual Studio Code で、統合ターミナルを開きます。

  2. 次の git コマンドを実行して、Microsoft のリポジトリから unit-tests という名前のブランチをフェッチし、そのブランチに切り替えます。

    git fetch upstream unit-tests
    git checkout -B unit-tests upstream/unit-tests
    

    このコマンドの形式を使用すると、upstream と呼ばれる、Microsoft の GitHub リポジトリからスタート コードを取得できます。 すぐに、origin と呼ばれる独自の GitHub リポジトリにこのブランチをプッシュします。

  3. 省略可能な手順として、Visual Studio Code で azure-pipelines.yml ファイルを開き、初期構成を確認することができます。 構成は、「Azure Pipelines を使用してビルド パイプラインを作成する」で作成した基本的なものに似ています。 アプリケーションのリリース構成のみをビルドします。

ローカルでテストを実行する

パイプラインにテストを送信する前に、ローカルですべてのテストを実行することをおすすめします。 ここでそれを行います。

  1. Visual Studio Code で、統合ターミナルを開きます。

  2. dotnet build を実行し、ソリューションで各プロジェクトをビルドします。

    dotnet build --configuration Release
    
  3. 次の dotnet test コマンドを実行し、単体テストを実行します。

    dotnet test --configuration Release --no-build
    

    --no-build フラグでは、プロジェクトを実行する前にビルドしないように指定します。 プロジェクトは前の手順でビルドしたため、ビルドする必要はありません。

    5 つのテストがすべて成功したことがわかるはずです。

    Starting test execution, please wait...
    A total of 1 test files matched the specified pattern.
    
    Passed!  - Failed:     0, Passed:     5, Skipped:     0, Total:     5, Duration: 57 ms
    

    この例では、テストの実行にかかった時間は 1 秒未満でした。

    合計で 5 つのテストがあったことに注目してください。 定義したテスト メソッドは 1 つだけ (FetchOnlyRequestedGameRegion) ですが、そのテストは 5 回 (TestCase インライン データで指定されているように、ゲーム マップごとに 1 回) 実行されます。

  4. テストをもう一度実行します。 今回は、--logger オプションを指定して、結果をログ ファイルに書き込みます。

    dotnet test Tailspin.SpaceGame.Web.Tests --configuration Release --no-build --logger trx
    

    出力から、TestResults ディレクトリに TRX ファイルが作成されたことがわかります。

    TRX ファイルは、テストの実行結果を含む XML ドキュメントです。 Visual Studio およびその他のツールが結果を視覚化するのに役立つ場合があるため、これはテスト結果には一般的な形式です。

    後で、パイプラインを介してテストが実行されるときに、テスト結果を視覚化して追跡するのに Azure Pipelines がどのように役立つかを確認します。

    Note

    TRX ファイルをソース管理に含めることは意図されていません。 .gitignore ファイルを使うと、Git で無視する一時ファイルや他のファイルを指定できます。 プロジェクトの .gitignore ファイルは、TestResults ディレクトリの内容を無視するように既に設定されています。

  5. 省略可能な手順として、Visual Studio Code で Tailspin.SpaceGame.Web.Tests フォルダーから DocumentDBRepository_GetItemsAsyncShould.cs ファイルを開き、テスト コードを調べることができます。 特に .NET アプリのビルドに関心がなくても、他の単体テスト フレームワークで目にする可能性があるコードと似ているため、このテスト コードが役立つ場合があります。

パイプライン構成にタスクを追加する

ここでは、単体テストを実行し、その結果を収集するようにビルド パイプラインを構成します。

  1. Visual Studio Code で、azure-pipelines.yml を次のように変更します。

    trigger:
    - '*'
    
    pool:
      vmImage: 'ubuntu-20.04'
      demands:
      - npm
    
    variables:
      buildConfiguration: 'Release'
      wwwrootDir: 'Tailspin.SpaceGame.Web/wwwroot'
      dotnetSdkVersion: '6.x'
    
    steps:
    - task: UseDotNet@2
      displayName: 'Use .NET SDK $(dotnetSdkVersion)'
      inputs:
        version: '$(dotnetSdkVersion)'
    
    - task: Npm@1
      displayName: 'Run npm install'
      inputs:
        verbose: false
    
    - script: './node_modules/.bin/node-sass $(wwwrootDir) --output $(wwwrootDir)'
      displayName: 'Compile Sass assets'
    
    - task: gulp@1
      displayName: 'Run gulp tasks'
    
    - script: 'echo "$(Build.DefinitionName), $(Build.BuildId), $(Build.BuildNumber)" > buildinfo.txt'
      displayName: 'Write build info'
      workingDirectory: $(wwwrootDir)
    
    - task: DotNetCoreCLI@2
      displayName: 'Restore project dependencies'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Build the project - $(buildConfiguration)'
      inputs:
        command: 'build'
        arguments: '--no-restore --configuration $(buildConfiguration)'
        projects: '**/*.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Run unit tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration)'
        publishTestResults: true
        projects: '**/*.Tests.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Publish the project - $(buildConfiguration)'
      inputs:
        command: 'publish'
        projects: '**/*.csproj'
        publishWebProjects: false
        arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/$(buildConfiguration)'
        zipAfterPublish: true
    
    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'
      condition: succeeded()
    

    このバージョンではこの DotNetCoreCLI@2 ビルド タスクが導入されます。

    - task: DotNetCoreCLI@2
      displayName: 'Run unit tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration)'
        publishTestResults: true
        projects: '**/*.Tests.csproj'
    

    このビルド タスクでは dotnet test コマンドが実行されます。

    このタスクでは、手動でテストを実行したときに使用した --logger trx 引数が指定されないことに注目してください。 publishTestResults 引数ではそれが自動的に追加されます。 この引数で、$(Agent.TempDirectory) 組み込み変数を使用してアクセスできる、一時ディレクトリに TRX ファイルを生成するようにパイプラインに指示されます。 また、タスクの結果がパイプラインに発行されます。

    projects 引数では、"**/*.Tests.csproj" と一致する C# プロジェクトがすべて指定されます。"**" 部分はすべてのディレクトリと一致し、"*.Tests.csproj" 部分は、ファイル名が ".Tests.csproj" で終わるすべてのプロジェクトと一致します。unit-tests ブランチには、Tailspin.SpaceGame.Web.Tests.csproj という単体テスト プロジェクトが 1 つだけ含まれます。 パターンを指定することで、ビルド構成を変更しなくても、さらに多くのテスト プロジェクトを実行できます。

GitHub にブランチをプッシュする

ここでは、変更を GitHub にプッシュし、パイプライン実行を確認します。 現在、unit-tests ブランチにいることを思い出してください。

  1. 統合ターミナルで、azure-pipelines.yml をインデックスに追加し、変更をコミットして、GitHub にブランチをプッシュします。

    git add azure-pipelines.yml
    git commit -m "Run and publish unit tests"
    git push origin unit-tests
    

Azure Pipelines でのテストの実行を監視する

ここでは、パイプラインでのテストの実行を確認してから、Microsoft Azure Test Plans からの結果を視覚化します。 Azure Test Plans では、アプリケーションのテストを正常に行うのに必要なすべてのツールが提供されます。 手動のテスト計画を作成して実行し、自動テストを生成し、利害関係者からフィードバックを収集することができます。

  1. Azure Pipelines で、手順ごとにビルドをトレースします。

    コマンド ラインから手動で行ったのと同じように、Run unit tests - Release タスクで単体テストが実行されていることがわかります。

    単体テストの実行によって得られるコンソール出力を示す Azure Pipelines のスクリーンショット。

  2. パイプラインの概要に戻ります。

  3. [テスト] タブに移動します。

    テストの実行の概要が示されます。 5 つのテストがすべて成功しています。

    [Test] タブが表示されている Azure Pipelines のスクリーンショット。合計 5 つのテストが実行され、成功率は 100% です。

  4. Azure DevOps で、[Test Plans] を選択してから、[Runs] を選択します。

    [Test Plans] セクションと [Runs] タブが強調表示されている Azure DevOps ナビゲーション メニューのスクリーンショット。

    先ほど実行したものを含め、最新のテストの実行が示されます。

  5. 最新のテストの実行をダブルクリックします。

    結果の概要が示されます。

    Azure DevOps で 5 つのテストが成功したことが表示された、テストの実行結果の概要を示すスクリーンショット。

    この例では、5 つのテストがすべて成功しています。 いずれかのテストが失敗した場合、ビルド タスクに移動して詳細情報を取得できます。

    また、TRX ファイルをダウンロードし、Visual Studio または別の視覚化ツールを使って調べることもできます。

追加したテストは 1 つだけですが、これはよい出発点であり、当面の問題は修正できます。 これで、チームでプロセスを改善する際に、さらにテストを追加して実行する場所が用意できました。

メインにブランチをマージする

実際のシナリオでは、その結果に満足したなら unit-tests ブランチを main にマージできます。ただしここでは、簡潔にするため、そのプロセスはスキップします。