気まま研究所ブログ

ITとバイク、思ったことをてきとーに書きます。

Azure Pipelinesで.Net FrameworkとNUnitのテスト実行とカバレッジ計測してみる

f:id:AonaSuzutsuki:20201203123648p:plain

すっごい今更ながらCI/CD環境を構築しておこうと思ってAzure Pipelineをイジイジしてました。
しかしながら、.Net FrameworkとNUnitの組み合わせだと情報がほぼ皆無と言っていいほど無く、構築に苦労しまくったので記録として残しておきます。
今回はNUnitの自動テストとコードカバレッジをPipeline上に表示させます。
ちなみにデプロイは手動でやるのでCIのみ構築します。

検証環境

項目 バージョン
.Net Framework 4.8
NUnit 3.12.0
NUnit.ConsoleRunner 3.11.1
OpenCover *1 4.7.922

Github上で公開しているSavannahXmlLibをPipelineに紐付けてCI環境を構築しました。
Visual Studio 2019でプロジェクトを作成してコードを書き、NUnitのテストプロジェクトを別途作成してテストを書いていたのをそのままCIに移行した感じです。
基本的にはXUnitと同じなのでうまく行かない場合はそちらも調べてみてください。
ちなみに今回作成したPipelinesはこれ

*1 カバレッジを計測する際に必要です。カバレッジを計測しないなら不要です。

Pipelineの作成

Pipelineを作る前にProjectを作成します。
適当に名前を付けて作成しましょう。
f:id:AonaSuzutsuki:20201203123454p:plain

プロジェクトを作成するとコードの場所を選択できるので、適当に選びます。
今回はGithub上にあるのでGithubを。
f:id:AonaSuzutsuki:20201203123508p:plain

Configureではいろいろありますが、とりあえず.Net Desktopを選択します。
どのみち後々書き換えるので何でも良いんですけどね。
f:id:AonaSuzutsuki:20201203123523p:plain

あとはYAMLテンプレートが表示されるので、適当にSaveしてRunします。
するとJobが走って自動でビルド・テストが実行されます。
f:id:AonaSuzutsuki:20201203123538p:plain

テストは成功しますが、Testsタブを開くとテストケースが0になってるはず。
これは後述しますが、Visual Studio Testになっているため、NUnitでのテストケースが実行されないためです。 f:id:AonaSuzutsuki:20201203123552p:plain

NUnitによる自動テスト

無事テストが走らなかったのでYAMLを書き換えてNUnitのテストケースを実行できるようにします。
Visual Studioでは不要でしたが、PipelinesではNUnit.ConsoleRunnerが必要なのでNugetで追加しておきましょう。(プッシュも忘れずに)
なお、この項のYAML全文はこちら

NUnit Consoleでテスト

タスクVSTest@2を削除し、右カラムからCommand lineを検索し、そのまま追加します。
するとCmdLine@2タスクが追加されるのでscriptの箇所をnunit3-console.exeでテスト実行するように書き換えます。

- task: CmdLine@2
  displayName: "NUnit Test"
  inputs:
    script: >
      packages\NUnit.ConsoleRunner.3.11.1\tools\nunit3-console.exe
      SavannahXmlLibTests\bin\$(buildConfiguration)\SavannahXmlLibTests.dll

説明のために直書きしてますが、exeファイルやdllパスなどはうまく変数にしてあげるといいでしょう。
.Net Desktopテンプレートならデフォルトで変数がいくつか設定されてるのでそれに習えばすぐにわかるはず。

これを実行するとログにテスト結果が表示され、ワーキングディレクトリ直下にTestResult.xmlが生成されます。

  ...
  Start time: 2020-12-02 03:11:09Z
    End time: 2020-12-02 03:11:11Z
    Duration: 1.230 seconds

Results (nunit3) saved as TestResult.xml

テスト結果を公開する

続いてテスト結果を公開します。
先のテスト実行だけではログに出力されるだけでTestsタブすら見えなくなってるはずです。

NUnit Consoleと同様に、「Publish Test Results」を探して適当に入力して追加します。
f:id:AonaSuzutsuki:20201203123620p:plain

属性 説明
Test result format NUnit テスト結果のフォーマットを指定します。
Test results files **\TestResult.xml テスト結果のxmlファイルを指定します。

YAMLだと次のようになります。

- task: PublishTestResults@2
  displayName: "Publish unit test result"
  condition: always()
  inputs:
    testResultsFormat: 'NUnit'
    testResultsFiles: '**\TestResult.xml'

conditionについては後述します。

f:id:AonaSuzutsuki:20201203123648p:plain
これでテスト結果が見られるようになりました。

OpenCoverによるコードカバレッジ計測

次にコードカバレッジも表示させることが可能です。
しかしながら、これがなかなかのハマりポイントで曲者でした。
なお、この項目でのYAMLはこちら

OpenCover.Consoleによるカバレッジ計測

カバレッジを計測するにはOpenCoverを使いますのでNugetで追加しておきましょう。

追加できたらNUnitテストタスクをOpenCoverを通すように変更します。

- task: CmdLine@2
  displayName: "NUnit & OpenCover"
  inputs:
    script: >
      packages\OpenCover.4.7.922\tools\OpenCover.Console.exe
      -register:Path64
      -target:"packages\NUnit.ConsoleRunner.3.11.1\tools\nunit3-console.exe"
      -targetargs:"SavannahXmlLibTests.dll"
      -targetdir:"SavannahXmlLibTests\bin\$(buildConfiguration)"
      -returntargetcode
      -output:"coverage.xml"
      -filter:"+[SavannahXmlLib]*"

オプションはいくつか指定していますが多いので別々に説明します。
基本的には公式のUsageに書いてあるのでそちらも合わせて。

属性 説明
-target ここにはOpenCoverが実行するアプリケーションを指定します。今回はNUnitの実行ファイルを指定しましょう。
-register コードカバレッジプロファイラーを登録する際の権限周りを指定するらしい?以前はuserでいけたらしいのですが、現在はPath32かPath64でしか正しく処理できません。Path32/64を指定するとOpenCover.Console.exeと同じディレクトリにある{x86/x64}\OpenCover.Profiler.dllが使用されるみたいです。
-targetargs ストプロジェクトのdllファイル名を指定します。
-targetdir テストプロジェクトのdllがあるデイレクトリを指定します。
-returntargetcode リターンコードをtargetで指定したプロセスのものを返すかどうか。これを指定するとNUnitのリターンコードが返されます。
-output カバレッジ情報を出力するXMLファイル名を指定します。
-filter カバレッジを計測する名前空間を指定します。書式についてはUsageや様々なブログで紹介されてるのでそちらをご覧ください。今回だと、テストプロジェクトや依存ライブラリを除いたSavannahXmlLib名前空間だけを計測します。

これらをうまく指定できたらNUnitの後にカバレッジが計測されます。
ちなみに失敗するとCommitting...の後にNo resultsと言われて0 Linesの計測結果がxmlに出力されます。

Committing...
Visited Classes 23 of 24 (95.83)
Visited Methods 104 of 130 (80)
Visited Points 367 of 437 (83.98)
Visited Branches 208 of 288 (72.22)

==== Alternative Results (includes all methods including those without corresponding source) ====
Alternative Visited Classes 23 of 27 (85.19)
Alternative Visited Methods 104 of 171 (60.82)

計測したカバレッジを変換する

カバレッジが計測できたら今度はReportGeneratorを用いてXML形式の結果をHTMLに変換するのと同時にCobertura形式のXMLファイルを出力します。
ReportGeneratorは予めMarketplaceからPipelinesへインストールする必要があります。

marketplace.visualstudio.com

インストールするとYAMLエディタ上でReportGeneratorがあるので適当に入力してOpenCoverによるNUnitテストの後に追加します。
基本的にオプションなどはUsageに書いてあるので詳細はそちらで。
f:id:AonaSuzutsuki:20201203123725p:plain

属性 説明
Reports **\coverage.xml OpenCoverで生成されたXMLファイルを指定します。
Target directory CoverageReport 結果を出力するディレクトリを指定します。どこでもいいですが、次のPublishで指定するので混乱しないように。
Report types HtmlInline_AzurePipelines;Cobertura AzurePipelines向けのHTMLドキュメント生成とCobertura形式のXMLファイルを生成します。
- task: reportgenerator@4
  condition: always()
  inputs:
    reports: '**\coverage.xml'
    targetdir: 'CoverageReport'

これを実行するとCoverageReport内にHTMLドキュメントとCobertura.xmlが生成されます。
とはいえArtifactsを公開しないと見えないので失敗してなければできてると思ってOK。

変換したカバレッジを公開する

最後に生成したXMLとHTMLからカバレッジを公開します。
同様に、Publish code coverage resultsを探し、適当に入力して追加します。
f:id:AonaSuzutsuki:20201203123741p:plain

属性 説明
Code coverage tool Cobertura カバレッジレポートを生成したツールを指定します。
Summary file CoverageReport\Cobertura.xml サマリーファイルを指定します。要するにReportGeneratorで生成したXMLファイルの事。ReportGeneratorではファイル名の指定はできないものの、ReportGeneratorのtargetdirの場所にフォーマット名.xmlで保存されています。
Report directory CoverageReport レポートとして公開するディレクトリを指定します。index.htmlかindex.htmがあればそれが表示されるようになっているのかと。
- task: PublishCodeCoverageResults@1
  condition: always()
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: 'CoverageReport\Cobertura.xml'
    reportDirectory: 'CoverageReport'

また、変数にてCoverageの自動生成をしないように設定しておきます。

variables:
  disable.coverage.autogenerate: 'true'

f:id:AonaSuzutsuki:20201203123757p:plain
するとCode Coverageタブが出現してReportGeneratorで生成したHTMLレポートが見られるはず。
なぜかSponsorとStarのアイコンが出ないけど。

ハマりポイント

何分情報が無さすぎてハマりポイントだらけでした。
コードカバレッジが曲者でこれだけで一日丸潰れした。

テストケースで失敗すると結果が公開されない

第一ハマりポイントはこれ、テストケースで失敗するとNUnit.Consoleがエラーを返すため結果が公開されません。

Evaluating: SucceededNode()
Result: False

というつれないログだけを残して公開処理が動きません。
これだと失敗すると公開処理が行われずにTestsタブが表示されないという本末転倒な話になるので、「condition: always()」で常に実行するように指定します。
すると失敗しても必ず公開されるようになります。

- task: PublishTestResults@2
  displayName: "Publish unit test result"
  condition: always()
  inputs:
    testResultsFormat: 'NUnit'
    testResultsFiles: '**\TestResult.xml'

OpenCoverのmissing pdbs

第二にめっちゃハマったポイント、なぜかpdbファイルがあるのにNo resultsとか言われてpdbファイルがないかregisterを見直せと言われる。

Committing...
No results, this could be for a number of reasons. The most common reasons are:
    1) missing PDBs for the assemblies that match the filter please review the
    output file and refer to the Usage guide (Usage.rtf) about filters.
    2) the profiler may not be registered correctly, please refer to the Usage
    guide and the -register switch.

なんのこっちゃいなと思ってローカルで動かしたら普通に動くし、なんならpdbファイル消しても動く。
はぁ?と思ってGithubのissueを眺めてるとありました。
OpenCover suddenly failing to output coverage in Azure DevOps #915

詳しくはわかりませんが、どうやら権限周りの問題かサンドボックスが問題らしい。
多分権限不足でプロファイラーの登録ができないんだと思う。
とりあえず-registerをPath32かPath64にすることで内部のDLLを使うようになるので回避できます。

packages\OpenCover.4.7.922\tools\OpenCover.Console.exe \
-register:Path64 \
...

auto-generating Html content警告

PublishCodeCoverageResultsタスクで出る警告ですが、どうやらReportGeneratorのHTMLにこのタスクが生成するHTMLで上書きしようとして出る警告らしい。

##[warning]Ignoring coverage report directory with Html content as we are auto-generating Html content

素直に変数で自動生成を無効にすると警告も消えます。

variables:
  disable.coverage.autogenerate: 'true'

Code Coverageが表示されない

ここまで全てうまく行ってもなぜかCode Coverageタブが表示されない現象がおきました。
処理の都合なのか、ブラウザのページをF5キーや更新ボタンで更新すれば出てくるはず。
にしても毎回開く度にリロードしないといけないのめんどくさすぎる。

おまけ: Visual Studio TestでNUnitテスト

標準のVSTestタスクでテストケースが実行されないのは、実行プロセスがvstest.console.exeで実行されるかつTestAdapterPathにNUnit3TestAdapterを指定していないからです。
逆に言えば、TestAdapterPathを指定してあげればNUnit.ConsoleRunner無しにVSTestでできます。

- task: VSTest@2
  inputs:
    testSelector: 'testAssemblies'
    testAssemblyVer2: |
      **\*test*.dll
      !**\*TestAdapter.dll
      !**\obj\**
    searchFolder: '$(System.DefaultWorkingDirectory)'
    pathtoCustomTestAdapters: 'packages\NUnit3TestAdapter.3.17.0\build\net35'
    codeCoverageEnabled: true

一応カバレッジ取ることができますが、HTMLやXMLではなく.coverage拡張子なのでCode Coverageタブがあっても直接は見ることができません。
ダウンロードして見ることもできますが、カバレッジを計測できないVisual Studio Communityではどちらにせよ見られないという罠付き。(dotCover入れててもダメだった)

雰囲気的にはOpenCoverと合わせればカバレッジも取れそうだけど、そこまでしてVSTestに拘りたくないしおまけということで。

参考文献

(Azure DevOps)Azure Pipelines で.NET Framework + xUnit のユニットテストの実行と計測方法について
Azure Pipelines 上でテストカバレッジをいい感じに表示する