Publishing code coverage in Azure DevOps and making it look pretty is way harder than it should be. It’s something that sounds simple, oh just check the box on the task – but nope you have to make sure to read the notes and add the additional parameter to the test task. Okay great, now you have a code coverage tab, but what is this .coverage file and how do I open it? That’s not very user friendly. And don’t get me started on having to wait for the entire pipeline to finish before you can even see the code coverage tab – nonsensical.

Default method – out of the box

If using the out of the box dotnet task with the test command, simply add the publishTestResults argument (or if using the task assistant, check the Publish test results and code coverage checkbox):

adding dotnet test task

.NET Core Task Assistant UI

However, if you read the information on the publishTestResults argument from the .NET Core CLI task (or clicking on the (i) on the Publish test results and code coverage option in the task assistant), it says:

Enabling this option will generate a test results TRX file in $(Agent.TempDirectory) and results will be published to the server. This option appends --logger trx --results-directory $(Agent.TempDirectory) to the command line arguments.

Code coverage can be collected by adding --collect "Code coverage" option to the command line arguments. This is currently only available on the Windows platform.


So even if you check the box, you need to ensure you add the --collect "Code coverage" argument. Oh, and you have to run this on a Windows agent, so no ubuntu-latest for us.

This produces code coverage that looks like the following in Azure DevOps.coverage file code coverage

How the default code coverage in Azure DevOps looks

It’s a link to a .coverage file which is great if you 1) have Visual Studio installed and 2) are on Windows (can’t open .coverage file on Mac).

The Better Way

The better way is to add the coverlet.collector NuGet package to (each of) the test project(s) that you run dotnet test against.

The easiest way to do this is to run the dotnet package add command targeting the test project:

dotnet add <TestProject.cspoj> package coverlet.collector

dotnet add packageAdding coverlet.collector with dotnet add package

For those who can’t run the dotnet command, add the following under the ItemGroup block in the .csproj file:

See the following page for the latest version.

Next, update the pipeline to ensure your dotnet test command looks like mine, and adding the PublishCodeCoverageResults@1 task.

The --collect:"XPlat Code Coverage" argument is what tells dotnet test to use the coverlet.collector package to generate us a cobertura code coverage report. As you can guess by the XPlat in the argument, this runs cross platform on both Windows and Ubuntu.

This argument creates a $(Agent.TempDirectory)/*/coverage.cobertura.xml code coverage report file. This folder is default output folder since Azure DevOps adds --results-directory /home/vsts/work/_temp to the command.

Next, we have to specifically add the PublishCodeCoverageResults@1 task to publish the code coverage output to the pipeline. It seems like at least with my project, it produces 2 coverage.cobertura.xml files and that throws a warning in the pipeline, so that’s I used $(Agent.TempDirectory)/*/coverage.cobertura.xml not $(Agent.TempDirectory)/**/coverage.cobertura.xml

duplicate code coverage reportsDuplicate coverage.cobertura.xml code coverage results

Now, after the entire pipeline has finished (including any of the deployment stages), we will have a code coverage tab with a way more visually appealing code coverage report: cobertura code coverage in azure devopsCobertura Code Coverage Report in Azure DevOps

Why not ReportGenerator?

I’ve seen many blog posts that are similar to mine, except that they have the reportgenerator@4 task. I used to think this was required, too! But I have recently found out it is not – at least not if you have ONE test project you are publishing results for.

If you have multiple test projects you would like code coverage published for, then yes, the reportgenerator@4 task is needed.

I have started to use the command line instead of the actual reportgenerator@4 task itself, though, as not every organization has the marketplace extension installed (and some have stricter policies about adding extensions than others).

reportgenerator@4 equivalent:

This needs to run before the PublishCodeCoverageResults@1 task, and that task needs to be updated just a little bit (the xml file path is different). See:

Code Coverage Tab Not Showing Up?

This doesn’t really solve the problem where Azure DevOps will not show the Code Coverage tab until the entire pipeline (including all deployments) has completed. In cases where code coverage is important, I either:

  1. Change the report output from $(Build.SourcesDirectory) to $(Build.ArtifactStagingDirectory) and publish the report as a pipeline artifact.
  2. Create a separate build pipeline than deployment pipeline. This harkens back to the day where we didn’t have multi-stage YAML builds and we had separate Build Definitions and Release Definitions. We can set up the deployment yml pipeline to be triggered from the completion of the build yml pipeline.


Now you know the ins and outs of adding Code Coverage to your .NET (Core) projects in Azure DevOps.

Good luck!