When setting up CI/CD (Continuous Integration and Continuous Deployment) pipelines in Azure DevOps for .NET 7+ solutions, such as those using ASP.NET Core, it’s important to setup an automated build. The automated build will need to use the dotnet
CLI to build the application, and run dotnet test
to execute the unit tests within the solution. These are the most common tasks configured within Azure DevOps Pipelines when setting up automated CI/CD workflows for .NET applications. The next items that are extremely valuable for monitoring the build is to integrate the Unit Test results and the Code Coverage results for those tests within the Azure Pipeline. This article will show you how to configure the YAML tasks for setting up Unit Test and Code Coverage integration within an Azure DevOps Pipeline.
Let’s do this!
Table of Contents
Basic Azure Pipeline YAML to Build .NET 7 Solution and Run Unit Tests
For this example, we’ll write an Azure Pipeline in YAML format that targets building and running the unit tests for a .NET 7 application written as a Visual Studio 2022 solution (.sln
file and related files) that is contained within a git Azure Repository.
The following is the basic YAML code needed to configure an Azure Pipeline for building this .NET 7 solution and running the Unit Tests:
# Automatically trigger on merges / commits to 'main' branch
trigger:
- main
# Specify build agent image
pool:
vmImage: 'windows-latest'
# Some config variables for pipeline
variables:
# specify the Build Configuration to build for the solution
buildConfiguration: 'debug' #'Release'
stages:
- stage: 'Build'
displayName: 'Build the web application'
jobs:
- job: 'Build'
displayName: 'Build job'
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '**/*.sln'
- task: UseDotNet@2
displayName: 'Use .NET SDK v7.x'
inputs:
version: '7.x'
- 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: 'Install .NET tools from local manifest'
inputs:
command: custom
custom: tool
arguments: 'restore'
- task: DotNetCoreCLI@2
displayName: 'Run Unit Tests - $(buildConfiguration)'
inputs:
command: 'test'
arguments: '--no-build --configuration $(buildConfiguration)'
publishTestResults: false
- 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()
This Azure Pipeline YAMl is configured to use the windows-latest
image in Azure DevOps for the Build Agent Pool. This is a managed image available from Microsoft in Azure DevOps. In most enterprise scenarios, you will host your own build agents. These build agents may even be configured with Linux instead. Whether you’re using a Windows or Linux build agent image, the YAMl pipeline configuration in this article should work just the same.
For clarity, the tasks configured in this Azure Pipeline YAML are as follows:
NuGetToolInstaller@1
– This task is configured to ensure that Nuget is installed on the Build Agent.NuGetCommand@2
– Call Nuget to restore dependencies for the solution.UseDotNet@2
– This task is configured to ensure that the .NET 7.x SDK is installed on the Build Agent.DotNetCoreCLI@2
– Thedotnet restore
command to restore dependencies.DotNetCoreCLI@2
– Thedotnet build
command to build the solution. This also specifies the--configuration
argument to pass in the Build Configuration to build; in this case it’s configured for thedebug
configuration using the$(buildConfiguration)
variable defined in the pipeline.DotNetCoreCLI@2
– Thedotnet tool restore
command to install .NET tools from the local manifest in the repository.DotNetCoreCLI@2
– Run the Unit Tests using thedotnet test
command. This also uses the--configuration
argument to pass in the Build Configuration to target.DotNetCoreCLI@2
– Thedotnet publish
command to publish the built projects as.zip
archives and save them to the artifact staging directory$(Build.ArtifactStagingDirectory)
for the pipeline.PublishBuildArtifacts@1
– Perform a Build Artifact Publish to the pipeline of the project.zip
files that were published in the previous task. This is so the build results of the pipeline are available later within the artifacts for the pipeline.
This is a great basic Azure Pipeline for a .NET 7 solution that will get you started configuring CI/CD for the solution. There are obviously custom configurations that may be needed, as well as publishing and deploying the app to your environment. The pipeline includes a publish step that publishes the built app to Artifacts on the Pipeline. For now, we’ll leave the remaining parts out as they are outside the scope of this article.
Next we’ll take a look at the primary components of this article that are integrating the Unit Test Results and Code Coverage Results to display in the Azure DevOps Pipeline. This will help you more easily manage and monitor the build.
Run Unit Tests with VSTest and Output Unit Test and Code Coverage Results Files
There are a couple different ways to run the Unit Tests for the .NET solution within the Azure Pipeline. The method chosen in this article using the DotNetCoreCLI@2
task to run the dotnet test
command with some arguments passed in. Let’s take a look at modifying this task to also output the Unit Test results and Code Coverage results!
The following is the basic task using DotNetCoreCLI@2
to run the dotnet test
command for executing the Unit Tests within the solution:
- task: DotNetCoreCLI@2
displayName: 'Run Unit Tests - $(buildConfiguration)'
inputs:
command: 'test'
arguments: '--no-build --configuration $(buildConfiguration)'
publishTestResults: false
This task is passing in the following 2 arguments to the dotnet test
command:
--no-build
– This tells it to skip building the solution. We’ll do this since it’s already been done previously in the pipeline.--configuration
– This is used to pass in the Build Configuration for the Visual Studio solution. In this example, the$(buildConfiguration)
variable within the pipeline is being used to configure the Build Configuration to use; which is configured todebug
.
Yes, the DotNetCLI@2
task has the publishTestResults
input that can be configured. However, when this is done, the task will not allow both Unit Test results and Code Coverage results to be published. So for now, we’ll set the publishTestResults
input to false
, so we can override this with our own configuration of arguments to output the results as needed.
To configure the dotnet test
command to output both Unit Test and Code Coverage results, a couple more arguments are needed for each.
Configure dotnet test
to Output Unit Test Results
The DotNetCoreCLI@2
task for running the Unit Tests (via dotnet test
command) requires additional arguments to configure it to output the Unit Test Results. The argument to use is the --logger
argument and will be used to configure the unit test results format to output. In this case, for VSTest format, the trx
value will be configured.
The following is the --logger
argument that’s required for this:
--logger trx
Next, the --results-directory
argument is required to tell the task to output the unit test results files to the desired directory. In this article, we’ll build the pipeline to use the $(Build.SourceDirectory)/TestResults/Coverage/
directory for this.
The following is the --results-directory
argument that is required for this:
--results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/"
The following is the task to run the Unit Tests with these required arguments for it to output the VSTest .trx
results files in the desired directory:
- task: DotNetCoreCLI@2
displayName: 'Run Unit Tests - $(buildConfiguration)'
inputs:
command: 'test'
arguments: '--no-build --configuration $(buildConfiguration) --logger trx --results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/"'
publishTestResults: false
Configure dotnet test
to Output Code Coverage Results
he DotNetCoreCLI@2
task for running the Unit Tests (via dotnet test
command) requires additional arguments to configure it to output the Code Coverage Results. The argument to use is the --collect
argument and will be used to configure the code coverage data collector to use. In this case, to use Coverlet, the value of XPlat Code Coverage
will be configured.
The following is the --collect
argument that’s required for this:
--collect "XPlat Code Coverage"
Next, the --results-directory
argument is required to tell the task to output the unit test results files to the desired directory. In this article, we’ll build the pipeline to use the $(Build.SourceDirectory)/TestResults/Coverage/
directory for this.
The following is the --results-directory
argument that is required for this:
--results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/"
The following is the task to run the Unit Tests with these required arguments for it to output the Coverlet Code Coverage Results files in the desired directory:
- task: DotNetCoreCLI@2
displayName: 'Run Unit Tests - $(buildConfiguration)'
inputs:
command: 'test'
arguments: '--no-build --configuration $(buildConfiguration) --results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/" --collect "XPlat Code Coverage"'
publishTestResults: false
Full DotNetCoreCLI@2
Task to Output Unit Test and Code Coverage Results
Now, let’s put the two examples of configuring the DotNetCoreCLI@2
task running the dotnet test
command together so we have a single YAML task that will output both the VSTest Unit Test Results and Coverlet Code Coverage Results.
The following is the full DotNetCoreCLI@2
task is configured to output both the unit test and code coverage results:
- task: DotNetCoreCLI@2
displayName: 'Run Unit Tests - $(buildConfiguration)'
inputs:
command: 'test'
arguments: '--no-build --configuration $(buildConfiguration) --logger trx --results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/" --collect "XPlat Code Coverage"'
publishTestResults: false
To use this in the Azure Pipeline YAML example shown previously, simply replace the Unit Test task within the previous Azure Pipeline YAML with this code. This will configure the pipeline to output the unit test and code coverage results. The next steps are to add tasks to the YAML that will publish these results to the Azure Pipeline.
Publish VSTest Unit Test Result to Azure Pipeline
Using the previous DotNetCoreCLI@2
task that runs dotnet test
and outputs the VSTest .trx
format Unit Test Results, the next step is to publish the unit test results to the Azure Pipeline.
Since the task that ran the Unit Tests using dotnet test
previously was configured to output the test results in .trx
format, then the PublishTestResults@2
task can be configured as follows:
- task: PublishTestResults@2
displayName: 'Publish Test Results'
inputs:
testResultsFormat: VSTest
testResultsFiles: '**/*.trx'
searchFolder: '$(Build.SourcesDirectory)/TestResults/Coverage/'
The following inputs on the task are configured as follows:
testResultsFormat
– Configured toVSTest
since VSTest was used bydotnet test
to run the unit tests and the results are in.trx
format.testResultsFiles
– Configured to the filter of**/*.trx
so it look for.trx
files to publish the unit test results from.searchFolder
– This is configured to the directory that the unit test results were output to. For the examples in this article it’s$(Build.SourcesDirectory)/TestResults/Coverage/
.
To use this task, it must be places after the Run Unit Tests
task in the Azure Pipeline YAML.
Publish Coverlet and Cobertura Code Coverage Results to Azure Pipeline
Using the previous DotNetCoreCLI@2
task that runs dotnet test
and outputs the VSTest .trx
format Unit Test Results, the next step is to publish the Code Coverage Results. The task is configured to output the code coverage results using Coverlet. Before these results can be published to the Azure Pipeline, the process chosen in this article requires a Code Coverage Report to be generated. To generate this report, we can use reportgenerator
.
The tasks required for this process to generate the code coverage report and publish it to the Azure Pipeline are as follows:
DotNetCoreCLI@2
– Thedotnet tool install
command will be used to install thereportgenerator
tool.PowerShell@2
– Thereportgenerator
tool will be called to generate the Code Coverage Report, using Cobertura, based on the Coverlet format Code Coverage Results that were previously output from the running the Unit Tests.PublishCodeCoverageResults@1
– Publish the Code Coverage Report that was generated usingreportgenerator
to the Azure Pipeline.
The following YAML are these 3 tasks configured as appropriate:
- task: DotNetCoreCLI@2
displayName: 'dotnet Tool Install "reportgenerator"'
inputs:
command: custom
custom: tool
arguments: 'install -g dotnet-reportgenerator-globaltool'
- task: PowerShell@2
displayName: 'Create Code Coverage Report'
inputs:
targetType: 'inline'
script: reportgenerator -reports:$(Build.SourcesDirectory)/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:HtmlInline_AzurePipelines
- task: PublishCodeCoverageResults@1
displayName: 'Publish Code Coverage Report'
inputs:
codeCoverageTool: 'cobertura'
summaryFileLocation: '$(Build.SourcesDirectory)/**/coverage.cobertura.xml'
Keep in mind, this YAML code is also configured for the reportgenerator
to look in the $(Build.SourcesDirectory)/CodeCoverage
directory using the targetdir
argument for the Code Coverage Results that were output from the dotnet test
command that ran the Unit Tests previously.
To use this task, it must be places after the Run Unit Tests
task in the Azure Pipeline YAML.
Azure Pipeline Display of Unit Test and Code Coverage Results
Once the Unit Test results and the Code Coverage results publishing has been integrated into the YAML pipeline, when the pipeline is executed there will be 2 new tabs added to the build results for the pipeline status. The overall Unit Test results pass/fail percentages and Code Coverage percentages will also be displayed on the main Summary view for the pipeline build results.
The following is a screenshot of the pipeline Summary view with these results shown:

The following tabs will be added:
- Tests – This tab will display the Unit Test results for the solution as published by the YAML pipeline.
- Code Coverage – This will display the Code Coverage Report that was generated and published by the YAML pipeline.
The following is a screenshot of the Tests tab for the Azure Pipeline showing the full Unit Test results that also allows you to drill into view the full unit test pass/fail results:

The following is a screenshot of the Code Coverage tab for the Azure Pipeline showing the full Code Coverage Report and results that also allows you to drill into view the full code coverage details:

Full Azure DevOps .NET 7 CI/CD Pipeline YAML with Unit Test and Code Coverage Integration
The following is a full example of the various YAML tasks discussed in this article put together as a full Azure Pipeline. The task for running the unit tests has been modified appropriately, and the Unit Test Publish and Code Coverage Publish tasks have be placed immediately before the “Publish the project” task.
# Automatically trigger on merges / commits to 'main' branch
trigger:
- main
# Specify build agent image
pool:
vmImage: 'windows-latest'
# Some config variables for pipeline
variables:
# specify the Build Configuration to build for the solution
buildConfiguration: 'debug' #'Release'
stages:
- stage: 'Build'
displayName: 'Build the web application'
jobs:
- job: 'Build'
displayName: 'Build job'
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '**/*.sln'
- task: UseDotNet@2
displayName: 'Use .NET SDK v7.x'
inputs:
version: '7.x'
- 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: 'Install .NET tools from local manifest'
inputs:
command: custom
custom: tool
arguments: 'restore'
# run unit tests and generate both:
# 1. Test results in .trx file format / VSTest format
# 2. Code coverage results in Cobertura file format
- task: DotNetCoreCLI@2
displayName: 'Run Unit Tests - $(buildConfiguration)'
inputs:
command: 'test'
arguments: '--no-build --configuration $(buildConfiguration) --logger trx --results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/" --collect "XPlat Code Coverage"'
publishTestResults: false
- task: PublishTestResults@2
displayName: 'Publish Test Results'
inputs:
testResultsFormat: VSTest
testResultsFiles: '**/*.trx'
searchFolder: '$(Build.SourcesDirectory)/TestResults/Coverage/'
- task: DotNetCoreCLI@2
displayName: 'dotnet Tool Install "reportgenerator"'
inputs:
command: custom
custom: tool
arguments: 'install -g dotnet-reportgenerator-globaltool'
- task: PowerShell@2
displayName: 'Create Code Coverage Report'
inputs:
targetType: 'inline'
script: reportgenerator -reports:$(Build.SourcesDirectory)/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:HtmlInline_AzurePipelines
- task: PublishCodeCoverageResults@1
displayName: 'Publish Code Coverage Report'
inputs:
codeCoverageTool: 'cobertura'
summaryFileLocation: '$(Build.SourcesDirectory)/**/coverage.cobertura.xml'
- 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()
Conclusion
I recently had the opportunity to configure a .NET solution build pipeline in Azure DevOps that need to integrate the Unit Test and Code Coverage results into the Azure Pipeline. This was a little tricky to figure out as it seems things changed a little with this in regards to .NET 7+ as compared to previously with .NET 6 and earlier. At least that was the impression that I got when looking up the steps and troubleshooting errors. So, I thought I’d write this article up to help both my future self and anyone else needing to do this.
Sure this may not be the best way to configure an YAML-based Azure Pipeline to publish the Unit Test and Code Coverage Results using VSTest, Coverlet, and Cobertura. There likely a couple other possible solutions, but this is the solution that I found worked for my project, so I thought I’d share it.
I hope this helped you, and happy automating those CI/CD pipelines in Azure DevOps!
Chris, thanks for this! It has been a struggle to find something that works for unit test and code coverage publishing together which seems like it should be a pretty standard process. A number of other articles seem to solve half of the equation but not the whole thing. IMHO it seems like .NET7 is not fully ready for use in Azure Pipelines based on what docs and troubleshooting are out there. Again thank you!
You’re welcome!!
Great Article. Thank you so much @Chris
Really clear article that’s helped me out loads. Thanks