Computing code coverage for a .NET Core project with Azure DevOps and Coverlet

Creating automated tests is important to be sure your application behaves as it should. The more tests you write, the more use cases are covered. Sometimes it can be hard to know which parts of the application are well tested, and which ones are not. That's why code coverage is interesting. It can be a useful measure to detect potential risk areas in the application (i.e. the most complex methods with the least coverage). Let's see how you can do that easily with .NET Core and Azure DevOps.

First, you need to compute the code coverage when you run the tests. There are multiple solutions, some are free such as coverlet, some are paid such as dotCover. In this post, I'll use coverlet. First, you need to install the NuGet package coverlet.msbuild. This package integrated directly with dotnet test as we'll see later.

<Project Sdk="Microsoft.NET.Sdk">

  ...

  <ItemGroup>
    <PackageReference Include="coverlet.msbuild" Version="2.5.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  ...

</Project>

You can test that everything works great by running the command dotnet test /p:CollectCoverage=true. After the test run, a json file should be created next to the csproj file. Azure DevOps doesn't support this file, but coverlet can output the result file in many standard formats. So, you can use the cobertura format which is supported by Azure DevOps.

dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura

After running the command for each test project, you get one code coverage file per project. These files are not very convenient to read. Let's generate a cool UI using ReportGenerator. This free tool allows you to generate a website to navigate into the files and see the lines actually covered by the tests. You can install as a .NET global tool:

dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:tests/**/coverage.cobertura.xml -targetdir:Report -reporttypes:HtmlInline_AzurePipelines;Cobertura

You now have a website that displays the code coverage in a convenient way. Note that there are multiple output formats. Here I use HtmlInline_AzurePipelines because at the end we would like to view this web site directly in Azure Pipelines, so it seems to be the best output for that.

Now let's integrate the tests and the code coverage on the build system (Azure Pipeline). Here's the yaml file that describes the build steps:

variables:
  buildConfiguration: Release
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1

steps:
# Install the latest version of the dotnet sdk
- task: DotNetCoreInstaller@0
  displayName: 'Use .NET Core sdk 2.2.103'
  inputs:
    version: 2.2.103

# build all projects
- task: DotNetCoreCLI@2
  displayName: dotnet build
  inputs:
    projects: 'src/**/*.csproj'
    arguments: '--configuration $(BuildConfiguration)'

# Run all tests with "/p:CollectCoverage=true /p:CoverletOutputFormat=cobertura" to generate the code coverage file
- task: DotNetCoreCLI@2
  displayName: dotnet test
  inputs:
    command: test
    arguments: '--configuration $(BuildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura'
    projects: 'tests/**/*.csproj'
    nobuild: true

# Generate the report using ReportGenerator (https://github.com/danielpalme/ReportGenerator)
# First install the tool on the machine, then run it
- script: |
    dotnet tool install -g dotnet-reportgenerator-globaltool
    reportgenerator -reports:$(Build.SourcesDirectory)/tests/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:HtmlInline_AzurePipelines;Cobertura
  displayName: Create Code coverage report

# Publish the code coverage result (summary and web site)
# The summary allows to view the coverage percentage in the summary tab
# The web site allows to view which lines are covered directly in Azure Pipeline
- task: PublishCodeCoverageResults@1
  displayName: 'Publish code coverage'
  inputs:
    codeCoverageTool: Cobertura
    summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml'
    reportDirectory: '$(Build.SourcesDirectory)/CodeCoverage'

When you run a build, you now have the code coverage result in the Summary tab of the build:

A new tab is also available: "Code Coverage". When you click on it, you'll see the generated web site.

If you want an example of coverlet and Azure DevOps, you can look at one of my GitHub project Meziantou.Framework and its first pipeline with coverlet integrated. You'll also notice that the result is reported on GitHub:

Voilà! Thanks to coverlet and ReportGenerator you can easily get a quick view of the parts of your applications that may need more tests.

Comments

Remy -

Coverlet exists as global tool too. So no need to add NuGet dependency to your test csproj

meziantou -

Hi Remy,

Using a global tool means you need to install it on the build machine. So, it would add one more build step to setup the machine. Also, if you share the same machine for multiple projects, you cannot have multiple versions installed at the same time which is not good… You could install it as a local tool but what's the difference with the NuGet package?

Leave a reply