今更ながら Azure DevOps 環境で c# (.NET Framework) アプリを CI/CD してみたいと思いまして、トライした内容をこちらで記載してみたいと思います。
"Azure DevOps" のアカウントを既に所有しているものとして記載します。
Step 1
[+ New Project]ボタンをクリックします。
Step 2
"Procjet name" など必要事項を記入および選択後、[Create]ボタンをクリックします。
私はこの後で超簡単な Counter プログラムを作成してリポジトリへ登録する予定なので、"Project name" を Counter として作成します。
下図はプロジェクト作成完了直後の画面です。
Pipeline を実行する対象となるプロジェクトを作成します。
ここでは以下のようなテストプロジェクトを作成して進めました。
Step 1
先ほど作成した Git プロジェクト(Counter)を Clone します。
下図あたりの表示にあるコマンドを使ったり、画面上のボタン操作を行うことで git clone します。
Step 2
Clone した場所へ Visual Studio で新しい c# プロジェクトを作成します。
私は以下のような環境で c# プロジェクトを作成しました。
[環境]
コンパイラ : | Visual Studio 2022, | Version 17.8.4 |
開発言語: | c# | |
フレームワーク: | .NET Framework 4.8 | |
テストフレームワーク: | MSTest.TestAdapter, | 3.1.1 |
MSTest.TestFramework, | 3.1.1 | |
NUnit, | 4.0.1 | |
NUnit3TestAdapter, | 4.5.0 | |
OS : | Windows11 home, | 23H2 |
重要
上記「テストフレームワーク」に記載の4つは、必ず NuGet で登録を行います。
NuGet 以外の方法で環境に登録すると、後述の Pipelines で期待する動作をすることが難しくなります。
代表的な Unit テストフレームワーク(NUnit、MSTestV1、MSTestV2)についても準備して動作を評価してみます。
プロジェクト名 | 内容 |
---|---|
CounterDll | 値(Integer)を管理する class Counter を提供する dll。 class Couter は、Increment, Decrement, Clear 3つのメソッドを持つ。 |
CounterForm | class Counter を使った WinForm アプリ。 |
CounterWPF | class Counter を使った WPF アプリ。 |
testCounter1 | CounterDll をテストする。NUnt 版。 |
testCounter2 | CounterDll をテストする。MSTestV1 版。 |
testCounter3 | CounterDll をテストする。MSTestV2 版。 |
Visual Studio 上の画面の様子です。
図: CounterForm 画面例
図: CounterWPF 画面例
Git (Azure DevOps) へコミット&プッシュします。手順については本ページの主たる内容から外れるので記載を割愛します。
コミット&プッシュ後の Azure DevOps 画面です。
以上で Pipelines を操作する前の事前準備を完了です。
Pipeline を作成していきます。
Step 1
画面左側の Pipeline ボタンをクリックすると下図のような画面を表示するので、[Create Pipeline]ボタンをクリックします。
Step 2
Pipeline の対象となるソースコードの所在を選択します。本ページでは "Azure Repos Git" を選択します。
Step 3
対象リポジトリを選択します。ここでは Counter を選択します。
Step 4
Pipeline を構成するプラットフォームを選択します。ここでは「.NET Desktop」を選択します。
Step 5
"azure-pipelines.yml" ファイルを生成してリポジトリへコミットされました。
内容を確認して必要に応じて修正します。まずは内容を確認してみましょう。
生成された yml ファイルのコードは以下のとおりです。
(1)NuGet パッケージの復元して、(2)ビルドして、(3)ユニットテストする、というような流れです。
["azure-pipelines.yml"]
# .NET Desktop # Build and run tests for .NET Desktop or Windows classic desktop solutions. # Add steps that publish symbols, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net trigger: - main pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)'
一部について説明します。
要素 | 内容 | 備考 |
---|---|---|
trigger | CI トリガーを取得するブランチを設定します。 | |
vmImage | ビルドするプラットフォームを設定します。Microsoft によってホストされるエージェントから指定します。 |
今回の例は VS2022 を使っているので 'windows-2022' を指定してもOKです。 |
NuGetToolInstaller@1 | インターネットまたはツール キャッシュから特定のバージョンの NuGet を取得し、PATH に追加します。 このタスクを使用して、NuGet タスクで使用される NuGet のバージョンを変更します。 | |
NuGetCommand@2 | このタスクを使用して、NuGet パッケージの復元、パック、プッシュ、または NuGet コマンドの実行を行います。 | |
VSBuild@1 | このタスクを使用して MSBuild を使用してビルドし、Visual Studio バージョン プロパティを設定します。 | |
VSTest@2 | このタスクを使用して、Visual Studio テスト (VSTest) ランナーを使用して単体テストと機能テスト
(Selenium、Appium、コード化された UI テストなど) を実行します。 Visual Studio テスト
アダプターを持つテスト フレームワークを実行できます。 フレームワークの例としては、MSTest、xUnit、NUnit、Chutzpah (QUnit、Mocha、Jasmine を使用する JavaScript テスト用) などがあります。 |
Step 6
いよいよ[Save and run]してみます。下図[Save and run]ボタンをクリックします。
下図画面を表示するので、もう一度[Save and run]ボタンをクリックします。
Pipeline を実行開始しました。
Pipeline を正常に完了ました。完了後の画面を以下に示します。
「✔ Job」のあたりをクリックすることで Job 詳細を確認することができます。
どうやら Pipeline は全て正常に処理できたようです。
以上から確認できたとりあえずの課題を以下に列挙します。
課題:
次章からこれら課題を順に解決していきます。
ビルド生成物(artifacts)を登録するように yml ファイルを修正します。
使用するコマンドは下記の2つです。
CopyFiles@2 で登録したいファイルを一か所に集めて、PublishBuildArtifacts@1 でアーティファクト登録する、というような手順です。
他:
要素 | 内容 | 備考 |
---|---|---|
CopyFiles@2 | 一致パターンを使用してソース フォルダーからターゲット フォルダーにファイルをコピーします。 (一致パターンは、フォルダー パスではなく、ファイル パスにのみ一致します)。 | |
PublishBuildArtifacts@1 | ビルド パイプラインでこのタスクを使用して、ビルド成果物を Azure Pipelines、TFS、またはファイル共有に発行します。 |
["azure-pipelines.yml"]
# .NET Desktop # Build and run tests for .NET Desktop or Windows classic desktop solutions. # Add steps that publish symbols, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net trigger: - main pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: CopyFiles@2 inputs: contents: CounterWPF\bin\$(buildConfiguration)\**\!(*.pdb) targetFolder: $(Build.ArtifactStagingDirectory) - task: CopyFiles@2 inputs: contents: | CounterForm\bin\$(buildConfiguration)\*.* !**\*.pdb targetFolder: $(Build.ArtifactStagingDirectory) - task: PublishBuildArtifacts@1 inputs: pathToPublish: $(Build.ArtifactStagingDirectory) artifactName: 'MyBuildOutputs' publishLocation: 'Container'
Pipeline 実行完了後の画面です。「1 published」に変わりました。
「1 published」あたりの画面をクリックすると下図 Artifacts 画面を表示します。
[・・・] ボタンウォリックすることであなたが希望する Artifacts をダウンロードなどすることができます。
前章では Artifacts として各ファイルを保存する例を紹介しました。しかしながら実際の運用ではアプリケーションは多くのファイルから構成されており、個別ファイルよりも必要となるファイル一式を zip などで圧縮したものを Artifacts として登録する方が都合が良い場合が多いと考えます。
本章では、事前に zip 圧縮したビルド生成物(artifacts)を登録するように yml ファイルを修正します。
使用するコマンドは下記の2つです。
ArchiveFiles@2 でフォルダをまとめて zip 圧縮して、PublishBuildArtifacts@1 でアーティファクト登録する、というような手順です。
他:
要素 | 内容 | 備考 |
---|---|---|
ArchiveFiles@2 | .7z、.tar、.gz、.zipなどの圧縮形式を使用してファイルをアーカイブします。 | |
PublishBuildArtifacts@1 | ビルド パイプラインでこのタスクを使用して、ビルド成果物を Azure Pipelines、TFS、またはファイル共有に発行します。 |
["azure-pipelines.yml"]
# .NET Desktop # Build and run tests for .NET Desktop or Windows classic desktop solutions. # Add steps that publish symbols, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net trigger: - main pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: ArchiveFiles@2 displayName: "Archive files" inputs: rootFolderOrFile: CounterWPF\bin\$(buildConfiguration) includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip' - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip' artifactName: 'MyBuildOutputs - zip' publishLocation: Container - task: ArchiveFiles@2 displayName: "Archive files" inputs: rootFolderOrFile: CounterForm\bin\$(buildConfiguration) includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip' - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip' artifactName: 'MyBuildOutputs - zip' publishLocation: Container
Pipelines 実行完了後の Artifacts 画面の様子は以下の通りです。
Unitテストを実行できるようにします。
現在の Warnings メッセージを見るとテスト用 dll のファイル名を変更するだけでもある程度動作しそうですが、任意のファイル名でテストできる方が今後の応用のためにも良いでしょう。そんな方向で進めてみます。
具体的には、VSTest@2 の inputs にて
testAssemblyVer2 という要素でテストアセンブリを具合的に指定することで解決できました。
詳細は下記例を参照ください。
要素 | 内容 | 備考 |
---|---|---|
VSTest@2 | このタスクを使用して、Visual Studio テスト (VSTest) ランナーを使用して単体テストと機能テスト
(Selenium、Appium、コード化された UI テストなど) を実行します。 Visual Studio テスト
アダプターを持つテスト フレームワークを実行できます。 フレームワークの例としては、MSTest、xUnit、NUnit、Chutzpah (QUnit、Mocha、Jasmine を使用する JavaScript テスト用) などがあります。 |
["azure-pipelines.yml"]
# .NET Desktop # Build and run tests for .NET Desktop or Windows classic desktop solutions. # Add steps that publish symbols, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net trigger: - main pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 inputs: testAssemblyVer2: | **\$(BuildConfiguration)\testCounter1.dll **\$(BuildConfiguration)\testCounter2.dll **\$(BuildConfiguration)\testCounter3.dll !**\obj\** platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: ArchiveFiles@2 displayName: "Archive files" inputs: rootFolderOrFile: CounterWPF\bin\$(buildConfiguration) includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip' - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip' artifactName: 'MyBuildOutputs - zip' publishLocation: Container - task: ArchiveFiles@2 displayName: "Archive files" inputs: rootFolderOrFile: CounterForm\bin\$(buildConfiguration) includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip' - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip' artifactName: 'MyBuildOutputs - zip' publishLocation: Container
以下、Pipelines 実行後の画面です。
Warnings が消えました。
16個のテストを実行して全てをパスしたことがわかります。
VSTest@2 で NUnit, MSTestV1, MSTestV2 を全部実行してくれたことがうれしい感じでした。
"Code Coverage" は空でした。
次章ではこの "Code Coverage" を有効化してみたいと思います。
参考
VSTest@2 は恐らく "vstest.console.exe" だと思われます。
ローカルPC上のコンソールにて下記コマンドにより同様に16個のテストを実行して同じ結果を得らえました。
vstest.console.exe ./testCounter*/bin/debug/testCounter*.dll
コード カバレッジは、ソースがどれくらいテストされているかを確認するのに役立つ指標および手法やツールのことを意味します。
本章では、前章で空だった "Code Coverage" タブで確認結果を表示できるようになるようにします。
Step 1
早速 VSTest@2 実施時にカバレッジ計測を追加してみましょう。VSTest@2 に「codeCoverageEnabled: true」の1行を追加するだけです。
["azure-pipelines.yml"]
# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net
trigger:
- main
pool:
vmImage: 'windows-latest'
variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'
- task: VSBuild@1
inputs:
solution: '$(solution)'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
- task: VSTest@2
inputs:
testAssemblyVer2: |
**\$(BuildConfiguration)\testCounter1.dll
**\$(BuildConfiguration)\testCounter2.dll
**\$(BuildConfiguration)\testCounter3.dll
!**\obj\**
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
codeCoverageEnabled: true
- task: ArchiveFiles@2
displayName: "Archive files"
inputs:
rootFolderOrFile: CounterWPF\bin\$(buildConfiguration)
includeRootFolder: true
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
artifactName: 'MyBuildOutputs - zip'
publishLocation: Container
- task: ArchiveFiles@2
displayName: "Archive files"
inputs:
rootFolderOrFile: CounterForm\bin\$(buildConfiguration)
includeRootFolder: true
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
artifactName: 'MyBuildOutputs - zip'
publishLocation: Container
Pipelines 実行後の「Code Coverage」画面です。
評価対象である CounterDll.dll および関係するアセンブリのコードカバレッジ結果を確認することができるようになりました。
Step 2
Step 1 の結果には不要なモジュール「nunit3.testadapter.dll 」のカバレッジ結果も含まれていました。関係はしているので含まれることは正常な動作と思いますが、これを削除したいと思いました。
これを実現する方法を説明したページがありました。
この内容に従って以下の修正を行います。
".runsetting" というファイルを新規に作成し、CodeCoverage の対象から除外したいファイルを <Exclude>~</Exclude> の部分に記載します。ファイル名は別の名称でも問題ありません。
[".runsetting"]
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="Code Coverage">
<Configuration>
<CodeCoverage>
<ModulePaths>
<Exclude>
<ModulePath>nunit3.testadapter.dll</ModulePath>
<!-- Add more ModulePath nodes here. -->
</Exclude>
</ModulePaths>
</CodeCoverage>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
そして、VSTest@2 に「runSettingsFile: '.\.runsettings'」の1行を追加するだけです。
["azure-pipelines.yml"]
# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net
trigger:
- main
pool:
vmImage: 'windows-latest'
variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'
- task: VSBuild@1
inputs:
solution: '$(solution)'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
- task: VSTest@2
inputs:
testAssemblyVer2: |
**\$(BuildConfiguration)\testCounter1.dll
**\$(BuildConfiguration)\testCounter2.dll
**\$(BuildConfiguration)\testCounter3.dll
!**\obj\**
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
codeCoverageEnabled: true
runSettingsFile: '.\.runsettings'
- task: ArchiveFiles@2
displayName: "Archive files"
inputs:
rootFolderOrFile: CounterWPF\bin\$(buildConfiguration)
includeRootFolder: true
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
artifactName: 'MyBuildOutputs - zip'
publishLocation: Container
- task: ArchiveFiles@2
displayName: "Archive files"
inputs:
rootFolderOrFile: CounterForm\bin\$(buildConfiguration)
includeRootFolder: true
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
artifactName: 'MyBuildOutputs - zip'
publishLocation: Container
以上の修正により、不要なモジュールの結果を除外することができました。
Step 3
以上の手順により、一応カバレッジを計測して結果を見ることができました。しかし、出力結果は HTML や XML ではなく .coverage 拡張子なので、"Code Coverage" 画面上でソースコードと対比してのカバレッジ確認をすることはできませんでした。結果をダウンロードすることもできますが、カバレッジ計測に対応している "Visual Studio Enterprise" 以外ではどちらにせよ内容を見られない、ということのようです。
ここまで頑張ってみましたが、ソースコードと対比してカバレッジを確認できないのは残念でした。
これを実現する方法について説明している記事がありました。
この内容に従って yml ファイルを以下のように修正します。
["azure-pipelines.yml"]
# .NET Desktop # Build and run tests for .NET Desktop or Windows classic desktop solutions. # Add steps that publish symbols, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net trigger: - main pool: vmImage: 'windows-latest' # vmImage: 'windows-2022' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 inputs: testAssemblyVer2: | **\$(BuildConfiguration)\testCounter1.dll **\$(BuildConfiguration)\testCounter2.dll **\$(BuildConfiguration)\testCounter3.dll !**\obj\** platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' searchFolder: '$(System.DefaultWorkingDirectory)' resultsFolder: '$(build.ArtifactStagingDirectory)/Test/Results' otherConsoleOptions: '/collect:"Code Coverage;Format=Cobertura"' # <<<< this is the important bit codeCoverageEnabled: true runSettingsFile: '.\.runsettings' # vv Then add publish coverage manually vv - task: PublishCodeCoverageResults@1 inputs: codeCoverageTool: 'Cobertura' summaryFileLocation: '$(build.ArtifactStagingDirectory)/Test/Results/**/*.xml' - task: ArchiveFiles@2 displayName: "Archive files" inputs: rootFolderOrFile: CounterWPF\bin\$(buildConfiguration) includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip' - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip' artifactName: 'MyBuildOutputs - zip' publishLocation: Container - task: ArchiveFiles@2 displayName: "Archive files" inputs: rootFolderOrFile: CounterForm\bin\$(buildConfiguration) includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip' - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip' artifactName: 'MyBuildOutputs - zip' publishLocation: Container
上記修正を行った後に実行した Pipelines 結果から "Code Coverage" の部分を以下に表示します。
無事にカバレッジ結果の詳細を閲覧できるようになりました。
"Branch coverage" も計測できており十分活用できそうです。とても良い感じになりました。
注意
(後日談として) カバレッジ結果をよく確認してみると、期待するカウント値とどうも違う結果になっていることに気づきました。まだ何かしら設定などで足りないところがある気がします。
後述の「7._OpenCover_でカバレッジ計測」はちゃんと動作してそうなので、当面は OpenCover による方法をご検討ください。
OpenCover は、.NET 2 以上(Windows OSのみ - MONOを除く)に対応したコードカバレッジツールです。32ビットと64ビットのプロセスをサポートし、ブランチとシーケンスポイントの両方をカバーします。
本章では OpenCover を使ってカバレッジ計測を行う方法について記載します。
ローカルPCで OpenCover によるカバレッジ計測をできるようになってから、次に Azure の Pipelines で OpenCover できるようにする、という順序で進めてみます。
最初に NuGet を使って下記2つのパッケージを追加します。
現在の環境を更新して以下に記載します。
[環境]
コンパイラ : | Visual Studio 2022, | Version 17.8.4 |
開発言語: | c# | |
フレームワーク: | .NET Framework 4.8 | |
テスト: | MSTest.TestAdapter, | 3.1.1 |
MSTest.TestFramework, | 3.1.1 | |
NUnit, | 4.0.1 | |
NUnit3TestAdapter, | 4.5.0 | |
OpenCover | 4.7.1221 | |
ReportGenerator | 5.2.0 | |
OS : | Windows11 home, | 23H2 |
"OpenCover.Console.exe" から "vstest.console.exe" を実行してテストを実行し、カバレッジ計測を行います。
そして出力されたカバレッジ計測結果を "ReportGenerator.exe" を使って HTML レポートを作成します。
作成したバッチファイル例を記載します。
["OpenCover.bat"]
rem Microsoft Visual Studio 2022 環境設定 call "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat" rem OpenCoverの実行 .\packages\OpenCover.4.7.1221\tools\OpenCover.Console.exe -register:Path64 -target:"vstest.console.exe" -targetargs:"./testCounter*/bin/debug/testCounter*.dll" -targetdir:"." -filter:"+[CounterDll]*" -output:OpenCoverReport.xml rem OpenCover の出力レポートを整形出力 .\packages\ReportGenerator.5.2.0\tools\net47\ReportGenerator.exe -reports:".\OpenCoverReport.xml" -reporttypes:html -targetdir:".\CoverageReport" rem レポートを表示 .\CoverageReport\index.html pause
パラメータ | 説明 |
---|---|
-target | OpenCover が実行するアプリケーションを指定します。今回は "VSTest.console.exe" を指定しています。 |
-register | "Path64" または "Path32" を指定します。この指定に従って "OpenCover.Console.exe" と同じディレクトリにある {x86/x64}\OpenCover.Profile.dll を使い分けるようです。 |
-targetargs | "-target" で指定したアプリケーションに対する引数を指定します。 上記例では "VSTest.console.exe" の引数としてテストプロジェクトの dll ファイルをワイルドカードを使って指定しています。 |
-targetdir | テストプロジェクトの dll があるディレクトリを指定します。ここで指定した場所からの相対パスで扱われる、と思われるので本例では "." を指定してみました。 |
-returntargetcode | これを指定すると、"-target" で指定したアプリケーションからの戻り値を OpenCover の戻り値として扱います。 |
-output | カバレッジ情報を出力する XML ファイル名を指定します。上記例では "OpenCoverReport.xml" というファイル名を指定しています。 |
-filter | カバレッジ計測する名前空間を指定します。 本例では "CounterDll" という名前空間を対象として指定しています。 書式については OpenCover の資料 や様々なインターネット記事を参照してください。 |
パラメータ | 説明 |
---|---|
-reports | OpenCover が出力した解析結果(XMLファイル)を指定します。 本ページ例では ".\OpenCoverReport.xml" を指定します。 |
-targetdir | 結果を出力するディレクトリを指定します。 本ページ例では ".\CoverageReport" を指定します。 |
-reporttypes | 出力するレポートタイプを指定します。複数指定することが可能です。 本ページ例では "html" を指定します。 指定できるタイプはとてもたくさんあります。こちら を参照。 |
ReportGenerator に関する参照ページ:
上記バッチファイルを実行した様子の例です。
出力されたレポート例です。
上図のクラス名(赤字部分)をクリックすると、下図のようなカバレッジ詳細を閲覧することができます。
ローカルPCで確認した手順を参考に、今度は "Azure DevOps" の Pipelines 上でコードカバレッジを表示できるようにします。
Step 1
VSTest@2 をコメントアウト(または削除)します。
["azure-pipelines.yml" (修正箇所のみの略記)]
・・・ - task: VSBuild@1 inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' # - task: VSTest@2 # inputs: # testAssemblyVer2: | # **\$(BuildConfiguration)\testCounter1.dll # **\$(BuildConfiguration)\testCounter2.dll # **\$(BuildConfiguration)\testCounter3.dll # !**\obj\** # platform: '$(buildPlatform)' # configuration: '$(buildConfiguration)' # codeCoverageEnabled: true # runSettingsFile: '.\.runsettings' - task: ArchiveFiles@2 displayName: "Archive files" inputs: rootFolderOrFile: CounterWPF\bin\$(buildConfiguration) includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip' ・・・
Step 2
"VSTest & OpenCover" の処理を追加します。"CmdLine@2" というタスクを使って実現します。
["azure-pipelines.yml"]
・・・ - task: CmdLine@2 displayName: "VSTest & OpenCover" inputs: script: > packages\OpenCover.4.7.1221\tools\OpenCover.Console.exe -target:"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" -targetargs:".\testCounter*\bin\$(BuildConfiguration)\testCounter*.dll /logger:trx;LogFileName=TestResult.xml" -targetdir:"." -register:Path64 -filter:"+[CounterDll]*" -output:"OpenCoverReport.xml" -returntargetcode ・・・
属性 | 説明 |
---|---|
-target | OpenCover が実行するアプリケーションを指定します。今回は "VSTest.console.exe" を指定しています。 |
-register | "Path64" または "Path32" を指定します。この指定に従って "OpenCover.Console.exe" と同じディレクトリにある {x86/x64}\OpenCover.Profile.dll を使い分けるようです。 |
-targetargs |
"-target" で指定したアプリケーションに対する引数を指定します。 上記例では "VSTest.console.exe" の引数として下記2つを指示しています。
|
-targetdir | テストプロジェクトの dll があるディレクトリを指定します。ここで指定した場所からの相対パスで扱われる、と思われるので本例では "." を指定してみました。 |
-returntargetcode | これを指定すると、"-target" で指定したアプリケーションからの戻り値を OpenCover の戻り値として扱います。 |
-output | カバレッジ情報を出力する XML ファイル名を指定します。上記例では "OpenCoverReport.xml" というファイル名を指定しています。 |
-filter | カバレッジ結果にアセンブリとクラスを選択的に含めたり除外したりするために適用するフィルターのリストを指定します。 本例では "CounterDll" というアセンブリに含む全てのクラスを対象として指定しています。 書式については OpenCover の資料 や様々なインターネット記事を参照してください。 |
Step 3
"VSTest@2" を削除しているので、新たにテスト結果の発行処理を追加します。"PublishTestResults@2" というタスクを使って実現します。
このタスク無しだと、"Tests" でのテスト結果表示が無くなります。
["azure-pipelines.yml"]
・・・ - task: PublishTestResults@2 displayName: "Publish Test Result" condition: always() inputs: testResultsFormat: 'VSTest' testResultsFiles: '**\TestResult.xml' ・・・
Step 4
ReportGenerator を使って OpenCover の出力レポートを整形します。
ReportGenerator は Azure DevOps 初期状態では使用できません。下記手順で Marketplace から無料で入手してインストールしておく必要があります。
[Browse marketplace]メニューをクリックします。
[Fig: "Marketplace" ボタン位置]
下図のような画面を表示します。
"ReportGenerator" で検索して、下図例では左側にある "ReportGenerator" をクリックします。
[Get it free]ボタンをクリックします。
[Install]ボタンをクリックします。
こちらの画面を表示したらインストールを完了です。[Proceed to organization]のボタンをクリックして元の画面へ戻る、またはこの画面を閉じても良いです。
これで Pipelines で "ReportGenerator" を使用できるようになりました。
"ReportGenerator" を使って OpenCover の出力レポートを整形します。"reportgenerator@5" というタスクを使って実現します。
["azure-pipelines.yml"]
・・・ - task: reportgenerator@5 condition: always() inputs: reports: '**\OpenCoverReport.xml' targetdir: 'CoverageReport' reporttypes: HtmlInline_AzurePipelines;Cobertura ・・・
属性 | 説明 |
---|---|
reports | OpenCover が出力した解析結果(XMLファイル)を指定します。 本ページ例では "OpenCoverReport.xml" を指定します。 |
targetdir | 結果を出力するディレクトリを指定します。 本ページ例では "CoverageReport" を指定します。 |
-reporttypes | 出力するレポートタイプを指定します。複数指定することが可能です。 本ページ例では "HtmlInline_AzurePipelines;Cobertura" を指定します。 指定できるタイプはとてもたくさんあります。"HtmlInline_AzurePipelines_Dark" へ変更するとダーク色をテーマとするページを出力してくれます。詳細は こちら を参照。 |
ReportGenerator に関する参照ページ:
Step 5
カバレッジ結果の発行処理を追加します。"PublishCodeCoverageResults@1" というタスクを使って実現します。
["azure-pipelines.yml"]
・・・ - task: PublishCodeCoverageResults@1 condition: always() inputs: codeCoverageTool: 'Cobertura' summaryFileLocation: 'CoverageReport\Cobertura.xml' reportDirectory: 'CoverageReport' env: DISABLE_COVERAGE_AUTOGENERATE: 'true' ・・・
属性 | 説明 |
---|---|
CodeCoverageTool | カバレッジレポートを生成したツールを指定します。 |
summaryFileLocation | サマリーファイルを指定します。ReportGenerator が出力する XML ファイルを指定します。ReportGenerator に対して出力するファイル名を指定することはできませんが、ReportGenerator の targetdir の場所に "フォーマット名.xml" で保存されています。 |
reportDIrectory |
レポートとして公開するディレクトリを指定します。 index.html または index.htm があればそれを表示していると思われます。 |
また下記指定を行うことで Coverage の自動生成をしないように設定します。
env:
DISABLE_COVERAGE_AUTOGENERATE: 'true'
これを設定しないと下図のような Warnings を出力します。
注意
"PublishCodeCoverageResults@2" という新しいタスクが既に公開されて使用可能ですが、これを使ってこの処理を期待通りに動作させることができませんでした。"PublishCodeCoverageResults@1" を使用するようにしてください。
解決方法を見つけたらこちらページの情報を更新したいと思います。
ここまでの内容を全て反映した "azure-pipelines.yml" の全体を以下に記載します。
["azure-pipelines.yml"]
# .NET Desktop # Build and run tests for .NET Desktop or Windows classic desktop solutions. # Add steps that publish symbols, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net trigger: - main pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' # - task: VSTest@2 # inputs: # testAssemblyVer2: | # **\$(BuildConfiguration)\testCounter1.dll # **\$(BuildConfiguration)\testCounter2.dll # **\$(BuildConfiguration)\testCounter3.dll # !**\obj\** # platform: '$(buildPlatform)' # configuration: '$(buildConfiguration)' # codeCoverageEnabled: true # runSettingsFile: '.\.runsettings' - task: CmdLine@2 displayName: "VSTest & OpenCover" inputs: script: > packages\OpenCover.4.7.1221\tools\OpenCover.Console.exe -target:"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" -targetargs:".\testCounter*\bin\$(BuildConfiguration)\testCounter*.dll /logger:trx;LogFileName=TestResult.xml" -targetdir:"." -register:Path64 -filter:"+[CounterDll]*" -output:"OpenCoverReport.xml" -returntargetcode - task: PublishTestResults@2 displayName: "Publish Test Result" condition: always() inputs: testResultsFormat: 'VSTest' testResultsFiles: '**\TestResult.xml' - task: reportgenerator@5 condition: always() inputs: reports: '**\OpenCoverReport.xml' targetdir: 'CoverageReport' reporttypes: HtmlInline_AzurePipelines_Dark;Cobertura - task: PublishCodeCoverageResults@1 condition: always() inputs: codeCoverageTool: 'Cobertura' summaryFileLocation: 'CoverageReport\Cobertura.xml' reportDirectory: 'CoverageReport' env: DISABLE_COVERAGE_AUTOGENERATE: 'true' - task: ArchiveFiles@2 displayName: "Archive files" inputs: rootFolderOrFile: CounterWPF\bin\$(buildConfiguration) includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip' - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip' artifactName: 'MyBuildOutputs - zip' publishLocation: Container - task: ArchiveFiles@2 displayName: "Archive files" inputs: rootFolderOrFile: CounterForm\bin\$(buildConfiguration) includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip' - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip' artifactName: 'MyBuildOutputs - zip' publishLocation: Container
Pipelines 実行後の結果を以下に紹介します。
[Summary]画面の様子です。
[Tests]画面の様子です。
[Code Coverage]画面の様子です。
"HtmlInline_AzurePipelines_Dark" を指定した場合の[Code Coverage]画面の様子です。
ソースコードでの詳細なカバレッジも画面上から表示できます。
良い感じです!
Pipelines で ClickOnce インストーラ作成を試してみました。私が期待する結果を得ることができましたので、こちらにメモを残します。
注意
本章に記載の内容はこれまで試したプロジェクトと環境が異なります。
下記[環境]に記載の条件で試していることにご注意ください。
[環境]
コンパイラ : | Visual Studio 2022, | Version 17.8.4 |
開発言語: | c# | |
フレームワーク: | .NET 8.0 | |
OS : | Windows11 home, | 23H2 |
Step 1
Visual Studio 上で「発行」から ClickOnce 設定を行って「ClickOnceProfile.pubxml」を設定します。
本ページは Pipelines の実現方法を記載することが目的なので、発行および ClickOnce についての説明を割愛します。
["ClickOnceProfile.pubxml" 例]
<?xml version="1.0" encoding="utf-8"?> <!-- https://go.microsoft.com/fwlink/?LinkID=208121. --> <Project> <PropertyGroup> <ApplicationRevision>1</ApplicationRevision> <ApplicationVersion>1.0.0.*</ApplicationVersion> <BootstrapperEnabled>True</BootstrapperEnabled> <Configuration>Release</Configuration> <CreateWebPageOnPublish>False</CreateWebPageOnPublish> <GenerateManifests>true</GenerateManifests> <Install>True</Install> <InstallFrom>Unc</InstallFrom> <InstallUrl>\\10.75.46.168\share$\ホゲホゲアプリ\</InstallUrl> <IsRevisionIncremented>True</IsRevisionIncremented> <IsWebBootstrapper>False</IsWebBootstrapper> <MapFileExtensions>True</MapFileExtensions> <MinimumRequiredVersion>1.0.0.0</MinimumRequiredVersion> <OpenBrowserOnPublish>False</OpenBrowserOnPublish> <Platform>Any CPU</Platform> <PublishDir>bin\Release\net8.0-windows10.0.22621.0\app.publish\</PublishDir> <PublishUrl>\\10.75.46.168\share$\ホゲホゲアプリ\</PublishUrl> <PublishProtocol>ClickOnce</PublishProtocol> <PublishReadyToRun>False</PublishReadyToRun> <PublishSingleFile>False</PublishSingleFile> <SelfContained>False</SelfContained> <SignatureAlgorithm>(なし)</SignatureAlgorithm> <SignManifests>False</SignManifests> <SkipPublishVerification>false</SkipPublishVerification> <TargetFramework>net8.0-windows10.0.22621.0</TargetFramework> <UpdateEnabled>True</UpdateEnabled> <UpdateMode>Foreground</UpdateMode> <UpdateRequired>True</UpdateRequired> <WebPageFileName>Publish.html</WebPageFileName> <ProductName>ホゲホゲアプリ</ProductName> <DisallowUrlActivation>True</DisallowUrlActivation> <PublisherName>ホゲ</PublisherName> </PropertyGroup> <ItemGroup> <BootstrapperPackage Include="Microsoft.NetCore.DesktopRuntime.8.0.x64"> <Install>true</Install> <ProductName>.NET デスクトップ ランタイム 8.0.1 (x64)</ProductName> </BootstrapperPackage> </ItemGroup> </Project>
Step 2
"ClickOnceProfile.pubxml" および発行に必要なファイルをリポジトリへコミットします。
Step 3
"azure-pipelines.yml" へ "- task: VSBuild@1" を下図のように追加します。
以上の内容だけだと発行先に不要なファイルを多数保存しています。DeleteFiles@1 で不要なファイルを削除します。下記ファイルおよびフォルダ以外は不要のはずです。
["azure-pipelines.yml"]
# .NET Desktop # Build and run tests for .NET Desktop or Windows classic desktop solutions. # Add steps that publish symbols, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net trigger: - main - develop - release/** - hotfix/** - feature/** pool: #vmImage: 'windows-latest' vmImage: 'windows-2022' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' zipFilename: 'HogeHogeApp' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSBuild@1 displayName: 'Publish' inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' msbuildArgs: '-t:publish -p:PublishProfile=Properties\PublishProfiles\ClickOnceProfile.pubxml -p:PublishDir="bin/Publish"' - task: DeleteFiles@1 displayName: 'Remove unneeded files' inputs: contents: | bin/Publish/HogeHogeApp.exe bin/Publish/Launcher.exe bin/Publish/*.dll bin/Publish/*.pdb bin/Publish/*.json - task: ArchiveFiles@2 inputs: rootFolderOrFile: bin\ includeRootFolder: true archiveType: 'zip' archiveFile: '$(Build.ArtifactStrageDirectory)\$(zipFilename)_$(Build.BuildId).zip' - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStrageDirectory)\$(zipFilename)_$(Build.BuildId).zip' artifactName: 'MyBuildOutputs' publishLocation: Container
Step 4
最後にあなたが希望する場所へデプロイします。
上記 "azure-pipelines.yml" 例では zip 圧縮してアーティファクトとして発行しています。
Step 5
デプロイした結果を取得して確認します。
下図は取得した zip ファイルを解凍した "bin/Publish/" フォルダ内の様子です。ClickOnce インストーラ一式が保存されていることを確認できます。
以上です。
本ページの情報は、特記無い限り下記 MIT ライセンスで提供されます。
2024-02-11 | - | 「6. カバレッジ計測」を更新 |
2024-02-06 | - | 「8. ClickOnce」を追加 |
2024-01-25 | - | 新規作成 |