Azure Pipelines で OpenCover/NUnit を実行し Codecov に送信

CubeSoft の各種プロジェクトでは、継続的インテグレーション (CI: Continuous Integration) 用サービスとして AppVeyor を利用していますが、諸々の事情を考慮して Azure Pipelines でも同等の CI を実行できるように環境の整備を進めています。この記事では、Azure Pipelines での CI、特に OpenCover/NUnit を用いてユニットテストを実行し、結果を Codecov に送信するまでの手順について記述します。

概要

前提として、何らかの csproj (多くの場合、ユニットテスト用のプロジェクト)に対して下記の PackageReference が記述されている事とします。

<ItemGroup>
    <PackageReference Include="NUnit" Version="3.11.0" />
    <PackageReference Include="NUnit.ConsoleRunner" Version="3.10.0" />
    <PackageReference Include="OpenCover" Version="4.7.922" />
</ItemGroup>

この状態で、下記のタスクを実行するように YAML ファイルを編集していきます。

  1. NuGet Resotre
  2. Build
  3. Run OpenCover with NUnit
  4. Send to Codecov
  5. Publish test results
  6. NuGet Pack
  7. Publish pipline artifacts

ここでは、ユニットテストに関係のある 3. ~ 5. の記述を抜粋します。下記を含む全ての記述内容は AzurePipelines.yml を参照下さい。*1

- script: >
    "$(TEST_TOOL)"
    -log:Error
    -register:user
    -target:"$(TEST_CORETOOL)"
    -targetargs:"$(PROJECT_NAME).Tests.dll"
    -targetdir:"Tests\$(PROJECT_BIN)"
    -returntargetcode
    -hideskipped:All
    -output:"$(TEST_COVERAGE)"
    -filter:"$(TEST_FILTERS)"
  displayName: 'Run tests via OpenCover and NUnit'

- script: |
    pip install codecov
    codecov -f "$(TEST_COVERAGE)" -t $(CODECOV_TOKEN)
  displayName: 'Send coverage results to Codecov'

- task: PublishTestResults@2
  inputs:
    testResultsFormat: 'NUnit'
    testResultsFiles: '**\$(TEST_RESULT)'
  displayName: 'Publish test results'

最初の task (script) で OpenCover を実行し、次の task で Codecov に結果を送信します。尚、この記事の執筆時点では、Azure Pipelines から Codecov に結果を送信するためにはトークンを指定する必要があります。そのため、How to integrate codecov.io in an Azure Build Pipeline を参考にして、あらかじめ CODECOV_TOKEN と言う環境変数を作成し、必要な値を Secret 設定で追加しておいて下さい。

Azure Pipelines の環境変数

最後の task で Azure Pipelines 上にテスト結果を送信します。PublishTestResults タスクを実行すると、各ビルドの Tests タブに結果が表示されるようになります。

Azure Pipelines 上でのテスト結果

備考

これ以降は、今回 Azure Pipelines 上で CI 環境を整えるにおいて、嵌まったポイント等いくつかの関連事項を記述します。

NuGet task を利用した Restore

NuGet に関連するコマンドは NuGet task が用意されており、Restore に関しても通常はこの task を利用します。

- task: NuGetCommand@2
  inputs:
    command: 'restore'
    restoreSolution: '$(PROJECT_NAME).sln'
    #feedsToUse: 'config'
    #nugetConfigPath: 'NuGet.config'
  displayName: 'Restore NuGet packages'

しかし、NuGetCommand@2 経由で実行した場合、初期設定ではカレントディレクトリに存在する NuGet.config は無視されます。config ファイルを反映させるには feedsToUse および nugetConfigPath 引数を利用するようなのですが、実際に試した所、今度は api.nuget.org が Source として認識しないような挙動を示しました。恐らく、明示的に config を指定した場合、この辺りも含めて設定する必要があるものと予想されます。

- script: |
    nuget restore "$(PROJECT_NAME).sln"
  displayName: 'Restore NuGet packages'

以上を考慮すると、NuGet.config を用意している場合、現時点では上記のように script で実行する方が楽なようです。

GitHub Releases からダウンロード

依存するライブラリが全て NuGet パッケージとして取得できれば良いのですが、必ずしもそうではない場合もあります。例えば、CubeICE は 7z.dll と言うライブラリに依存しています。ここでは、このライブラリを Releases - cube-soft/7z から取得する事を試みます。

- task: DownloadGitHubRelease@0
  inputs:
    connection: 'cube-soft-ci'
    userRepository: 'cube-soft/7z'
    itemPattern: '7z-*-x64.zip'
    downloadPath: '$(Build.SourcesDirectory)'
  displayName: 'Download 7-Zip modules'

GitHub Releases からのダウンロードには Download GitHub Release task を利用します。userRepository に対象となるリポジトリの名前、itemPattern にダウンロードするファイルを表す文字列、downloadPath に保存ディレクトリのパスを指定します。

connection には、Service connections と呼ばれる機能で作成した文字列を記述します。作成手順は、まず Creating a personal access token for the command line を参考に、GitHub 上でトークンを生成します(生成時に指定するスコープに関しては、repo, user, admin:repo_hook が推奨されているようです)。次に、Azure Pipelines の左下にある Project settings から Service connections を選択し、New service connection で GitHub を選択します。

Service connection の新規作成

新規作成画面で Personal access token を選択し、Connection Name には適当な名前、Token には GitHub で取得したトークンを入力します。最後に、ここで設定した名前を DownloadGitHubRelease@0 の connection 引数に記述すると完了です。

Pipeline Artifacts の設定

Azure Pipelines には Artifacts と言う項目が存在します。これは、Azure DevOps の Artifacts(左側にメニューとして表示されている項目)とは別物で、各ビルド結果の右上に表示されるリンクから辿る事ができます。

Pipeline Artifacts

この Artifacts に成果物を表示するには、Publish Pipeline Artifact task を利用します。

- task: PublishPipelineArtifact@0
  inputs:
    artifactName: '$(PROJECT_NAME)'
    targetPath: '$(Build.ArtifactStagingDirectory)'
  displayName: 'Publish pipline artifacts'

NuGet Pack など多くの task において、実行結果は $(Build.ArtifactStagingDirectory) に保存されます。そのため、targetPath 引数にはこの変数を指定しておくと良いようです。

*1:Cube プロジェクトでは、取得した各種 NuGet パッケージを "../packages" に配置するように設定しています(参考:Cube のプロジェクト構成およびビルド&テスト方法)。初期設定では、これらの NuGet パッケージは "$(UserProfile)/.nuget/packages" に配置されるようなので、もしリンク先の YAML ファイルを利用する場合には、適当に置き換えて下さい。