By code coverage, we mean the action of trying to measure how much of our code has been executed by our tests. This sound like


Untested code is a broken code. Definitely a strong statement but true in a way, we don’t always manage to get enough coverage. Often this happens because we don’t have time, other times because despite having written tests we are not able to read the metrics.

So, how we can “humanize” code coverage metrics? And how we can generate its?

To answer at these questions I usually use two libraries.

to gather metrics, and

for generate human-readable reports.

How can set-up coverlet?

I usually include coverlet.msbuild by MSBuild .targets Files - Visual Studio | Microsoft Docs.

 1<?xml version="1.0" encoding="utf-8"?>
 4  <PropertyGroup>
 5    <CollectCoverage>true</CollectCoverage>
 6    <CoverletOutputFormat>cobertura</CoverletOutputFormat>
 7    <CoverletOutput>$(ArtifactsCoverageDir)\$(MSBuildProjectName).xml</CoverletOutput>
 8  </PropertyGroup>
10  <ItemGroup>
11    <PackageReference Include="coverlet.msbuild" Version="$(CoverletMSBuildVersion)" IsImplicitlyDefined="true" PrivateAssets="all" Publish="true" />
12  </ItemGroup>

For alternative ways to include coverlet into yout test project see also coverlet-coverage/coverlet: Cross platform code coverage for .NET (

How can set-up ReportGenerator?

In keeping with above to include ReportGenerator by MSBuild .targets Files - Visual Studio | Microsoft Docs.

 1<?xml version="1.0" encoding="utf-8"?>
 4  <ItemGroup>
 5    <PackageReference Include="ReportGenerator" Version="$(ReportGeneratorVersion)" IsImplicitlyDefined="true" PrivateAssets="all" Publish="true" />
 6  </ItemGroup>
 8  <Target Name="GenerateCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
 9    <ItemGroup>
10      <CoverageFiles Include="$(ArtifactsCoverageDir)\$(MSBuildProjectName).xml" />
11    </ItemGroup>
13    <ReportGenerator ProjectDirectory="$(MSBuildProjectDirectory)" ReportFiles="@(CoverageFiles)" TargetDirectory="$(ArtifactsReportDir)\$(MSBuildProjectName)\Reports" ReportTypes="Html;Latex" HistoryDirectory="$(ArtifactsReportDir)\$(MSBuildProjectName)\History" VerbosityLevel="Verbose" />
14  </Target>

Also this tool offer a various way to use it, you can find all ways onto official documentation ReportGenerator - converts coverage reports generated by coverlet.

How to wire-up all that?

To make everything work we need to add another MSBuild file.

 1<?xml version="1.0" encoding="utf-8"?>
 4  <PropertyGroup>
 5    <VSTestLogger>trx</VSTestLogger>
 6    <VSTestResultsDirectory>$(ArtifactsTestResultsDir)</VSTestResultsDirectory>
 7  </PropertyGroup>
 9  <Import Project="CollectCoverage.targets" />
10  <Import Project="ReportGenerator.targets" />

And include this into your test project, something like this

1<Project Sdk="Microsoft.NET.Sdk">
3  <PropertyGroup>
4    <TargetFramework>net5.0</TargetFramework>
5  </PropertyGroup>
7  <Import Project="Tests.targets" />

Now everything you are able to run dotnet test you will able to inspect and analyze something like this

Example of coverage report
Example of coverage report for a single file

I think that is an amazing tool to understand at a glance which codes are covered and which not.

And now, how I can put it into Azure DevOps pipeline?

It would be nice if this report came was published into the Build pipeline report, don’t you think? Maybe even include branch policies for it.

Well that’s possible by use Publish Code Coverage Results task, something like this:

1- task: [email protected]
2  displayName: Publish Code Coverage Results
3  inputs:
4    codeCoverageTool: 'cobertura'
5    summaryFileLocation: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)/Reports/Summary/Cobertura.xml'
6  continueOnError: true
7  condition: always()

We notice the summaryFileLocation argument, this means that we will push only one file to Azure DevOps why?

One unwrite note of Publish Code Coverage Results task or limitation, I don’t know, is that the sum of covered lines, when we publish more reports, is take from the first file

Glance to summary code coverage percentage reported by Azure DevOps

This results in an unreliable result.

To fix that problem we can marge multiple reports into a summary reports so that can be publish it only one. One way to make it is the follow

 1<?xml version="1.0" encoding="utf-8"?>
 2<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="GenerateSummaryCoverageReport" xmlns="">
 4  <PropertyGroup>
 5    <TargetFramework>net5.0</TargetFramework>
 6  </PropertyGroup>
 8  <UsingTask TaskName="ReportGenerator" AssemblyFile="$(NuGetPackageRoot)reportgenerator\$(ReportGeneratorVersion)\tools\$(TargetFramework)\ReportGenerator.MSBuild.dll" />
10  <Target Name="GenerateSummaryCoverageReport" DependsOnTargets="Restore">
11    <ItemGroup>
12      <CoverageFiles Include="$(ArtifactsCoverageDir)\*.xml" />
13    </ItemGroup>
15    <ReportGenerator ProjectDirectory="$(MSBuildProjectDirectory)" ReportFiles="@(CoverageFiles)" TargetDirectory="$(ArtifactsTestResultsDir)\Reports\Summary" ReportTypes="Cobertura" />
16  </Target>

and run MSBuild project into the pipeline with

1- script: dotnet msbuild SummaryReportGenerator.proj /p:Configuration=$(Configuration)
2  name: GenerateCodeCoverageSummary
3  displayName: Generate code coverage summary

Once you’ve done this the sum of covered lines on Build pipeline will true.

The greenfield approch.

All above is fully automated into MsBullet from version 0.6.1.

If you are approaching a greenfield then you might find this getting started useful.

If, on the other hand, you are intent on using it in a brownfield and need some tips, leave a comment below.
If you need to share sensitive information feel free to contact me privately!