Easy, Automated Code Coverage for .NET Core
Tags: dotnetcore, cake, travisci, coveralls
Published: 2018-07-22 18:27:18

---
title: Easy, Automated Code Coverage for .NET Core
published: true
description: Generating .NET Core code coverage reports in TravisCI and posting to coveralls.io
tags: dotnet-core, cake, travis-ci, coveralls
---

For my projects, I like to use [TravisCI](https://travis-ci.org/) for running my builds and tests. If I'm working with a language that supports it, I also like to publish code coverage results from the test run to [coveralls.io](https://coveralls.io/). A little while back I found a cross-platform tool to calculate coverage results for .NET Core projects: {% github lucaslorentz/minicover %}

All of the example code for this post is available on my GitHub profile: {% github nlowe/coveralls-netcore %}

## Using MiniCover

MiniCover is distributed as a `dotnet` cli tool. We either need to add it as a `DotNetCliToolReference` to one of the top-level projects in the repo or as a dedicated tools project. I like to place the reference in `./minicover/minicover.csproj` for reasons that will become clear soon:

```xml
<!-- ./minicover/minicover.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <IsPackable>false</IsPackable>
  </PropertyGroup>
  
  <ItemGroup>
    <DotNetCliToolReference Include="MiniCover" Version="2.0.0-ci-20180517205544" />
  </ItemGroup>

</Project>
```

Be sure to run `dotnet restore` on this project.

This is required until MiniCover is updated to support installation as a global tool (added in .NET Core 2.1): {% github https://github.com/lucaslorentz/minicover/issues/90 %}

Since it's still being actively developed, MiniCover is distributed as prerelease packages. Check the [nuget feed](https://www.nuget.org/packages/MiniCover/) to get the latest version.

### Generating Coverage

We're ready to run our tests with coverage:

#### Build the project

Newer versions of .NET Core include an implicit restore here. If you're using an old version of .NET Core be sure to do a `dotnet restore` first.

```bash
dotnet build
```

#### Instrument the assemblies under test.

This inserts IL instructions into your assemblies to write coverage to a results file:

```bash
cd minicover
dotnet minicover instrument --workdir ../ --assemblies test/**/bin/**/*.dll --sources src/**/*.cs
dotnet minocover reset
cd ..
```

#### Run your tests:

You have to tell `dotnet` not to rebuild assemblies when running tests. Otherwise the instrumentation we just added will be overwritten and no coverage will be tracked.

```bash
for project in test/**/*.csproj; do dotnet test --no-build $project; done
```

#### Generate Reports:

MiniCover includes support for rendering many types of reports. The most basic type is a Console report with a fatal threshold. If coverage is below this threshold, it will exit with a non-zero exit code allowing you to fail the build if coverage is too low.

```bash
cd minicover
dotnet minicover report --workdir ../ --threshold 90
cd ..
```

Here's a sample of what this looks like (In a TTY that supports colors, files above the threshold will be green and files below the threshold will be red):

```bash
+-------------------+-------+---------------+------------+
| File              | Lines | Covered Lines | Percentage |
+-------------------+-------+---------------+------------+
| src/MyLib/Math.cs |    2  |        1      |   50.000%  |
+-------------------+-------+---------------+------------+
| All files         |    2  |        1      |   50.000%  |
+-------------------+-------+---------------+------------+
```

#### Uninstrument Assemblies

If you are planning on publishing artifacts for this build, be sure to uninstrument assemblies:

```bash
cd minicover
dotnet minicover uninstrument --workdir ../
cd ..
```

## Simplify things with Cake

While this is a great way to get code coverage results, it is a little tedious. For my .NET Projects, I prefer to use `cake` to script the build process: {% github cake-build/cake %}

To get started with cake, you will need two files, a `build.cake` build script and a platform-specific bootstrapper script. Since we can build and generate coverage for .NET Core projects on Windows, macOS, and Linux, we can include a PowerShell script for Windows and a Bash Script for the other platforms. If you want to get started with building a .NET Core Project with cake, feel free to use my bootstrapper scripts: {% github nlowe/cake-bootstrap-dotnet %}

Cake is extremely extensible via the `addin` system. This restores extensions to Cake as a `nuget` package before running the build script. I've made a Cake addin for minicover that greatly simplifies the coverage generation process: {% github nlowe/Cake.MiniCover %}

### The Build Script

Cake organizes the build script as a series of `Task`s with dependencies. Addins expose aliases for us to call.

#### Referencing the Addin

The first order of business is referencing our addin:

```csharp
#addin "nuget:?package=Cake.MiniCover&version=0.29.0-next20180721071547&prerelease"
```

> **Note**: I recently fixed support for publishing coverage to https://coveralls.io. For now, you will need to reference the `0.29.0-next20180721071547` pre-release of `Cake.MiniCover` until `0.29.0` is released.
>
> Additionally, this version of `Cake.MiniCover` requires `Cake` 0.29+. You will need to use the bootstrap scripts from the example repo I linked at the beginning of this post or update them to reference `Cake` 0.29+

#### Locating the Tools Project

Next, we need to tell Cake where our MiniCover tools project is located:

```csharp
SetMiniCoverToolsProject("./minicover/minicover.csproj");
```
#### Writing the Test Target

We can wrap our [`DotNetCoreTest`](http://cakebuild.net/api/Cake.Common.Tools.DotNetCore/DotNetCoreAliases/8191BBC4) call in a [`MiniCover`](http://cakebuild.net/api/Cake.MiniCover/MiniCoverAliases/6959692A) call. This will automatically instrument and uninstrument assemblies and run any reports we request:

```csharp
Task("Test")
    .IsDependentOn("Build")
    .Does(() => 
{
    MiniCover(tool => 
        {
            foreach(var project in GetFiles("./test/**/*.csproj"))
            {
                DotNetCoreTest(project.FullPath, new DotNetCoreTestSettings
                {
                    Configuration = configuration,
                    NoRestore = true,
                    NoBuild = true
                });
            }
        },
        new MiniCoverSettings()
            .WithAssembliesMatching("./test/**/*.dll")
            .WithSourcesMatching("./src/**/*.cs")
            .WithNonFatalThreshold()
            .GenerateReport(ReportType.CONSOLE)
    );
});
```

For brevity I've only included the `Test` task here. Please see the [full build script](https://raw.githubusercontent.com/nlowe/coveralls-netcore/master/build.cake) for the `Build` task definition and other variables required to execute this task.

Note the `WithNonFatalThreshold()` setting. This will disable exiting with an error if coverage is below the specified threshold, which is useful if you want your build system to track builds but have a different system for tracking coverage.

### Generating Coverage Results

Now, we can simply invoke our build script to run our unit tests and print a coverage report to the console:

```bash
# On macOS / Linux:
./build.sh -t test
```

Or

```powershell
# Windows:
./build.ps1 -t Build
```

Cake will figure out the task dependencies and execute them in the proper order, showing output and keeping track of how long each task takes to run:

```bash
$ ./build.sh -t test
Installing Cake 0.29.0
  Writing /tmp/tmpcaiVOk.tmp
info : Adding PackageReference for package 'Cake.CoreCLR' into project '/home/nathan/projects/coveralls-netcore/tools/cake.csproj'.
log  : Restoring packages for /home/nathan/projects/coveralls-netcore/tools/cake.csproj...
info :   GET https://api.nuget.org/v3-flatcontainer/cake.coreclr/index.json
info :   OK https://api.nuget.org/v3-flatcontainer/cake.coreclr/index.json 135ms
info :   GET https://api.nuget.org/v3-flatcontainer/cake.coreclr/0.29.0/cake.coreclr.0.29.0.nupkg
info :   OK https://api.nuget.org/v3-flatcontainer/cake.coreclr/0.29.0/cake.coreclr.0.29.0.nupkg 49ms
log  : Installing Cake.CoreCLR 0.29.0.
info : Package 'Cake.CoreCLR' is compatible with all the specified frameworks in project '/home/nathan/projects/coveralls-netcore/tools/cake.csproj'.
info : PackageReference for package 'Cake.CoreCLR' version '0.29.0' added to file '/home/nathan/projects/coveralls-netcore/tools/cake.csproj'.
Analyzing build script...
Processing build script...
Installing addins...
Compiling build script...
  Restoring packages for /home/nathan/projects/coveralls-netcore/minicover/minicover.csproj...
  Restore completed in 73.51 ms for /home/nathan/projects/coveralls-netcore/minicover/minicover.csproj.
  Generating MSBuild file /home/nathan/projects/coveralls-netcore/minicover/obj/minicover.csproj.nuget.g.props.
  Generating MSBuild file /home/nathan/projects/coveralls-netcore/minicover/obj/minicover.csproj.nuget.g.targets.
  Restore completed in 180.67 ms for /home/nathan/projects/coveralls-netcore/minicover/minicover.csproj.

========================================
Restore
========================================
Executing task: Restore
  Restoring packages for /home/nathan/projects/coveralls-netcore/test/MyLib.Tests/MyLib.Tests.csproj...
  Restoring packages for /home/nathan/projects/coveralls-netcore/src/MyLib/MyLib.csproj...
  Restore completed in 63.88 ms for /home/nathan/projects/coveralls-netcore/test/MyLib.Tests/MyLib.Tests.csproj.
/home/nathan/projects/coveralls-netcore/src/MyLib/MyLib.csproj : warning NU1603: MyLib depends on NETStandard.Library (>= 2.0.2-servicing-26420-0) but NETStandard.Library 2.0.2-servicing-26420-0 was not found. An approximate best match of NETStandard.Library 2.0.2 was resolved. [/home/nathan/projects/coveralls-netcore/coveralls-netcore.sln]
  Generating MSBuild file /home/nathan/projects/coveralls-netcore/test/MyLib.Tests/obj/MyLib.Tests.csproj.nuget.g.props.
  Generating MSBuild file /home/nathan/projects/coveralls-netcore/src/MyLib/obj/MyLib.csproj.nuget.g.props.
  Generating MSBuild file /home/nathan/projects/coveralls-netcore/test/MyLib.Tests/obj/MyLib.Tests.csproj.nuget.g.targets.
  Generating MSBuild file /home/nathan/projects/coveralls-netcore/src/MyLib/obj/MyLib.csproj.nuget.g.targets.
  Restore completed in 655.78 ms for /home/nathan/projects/coveralls-netcore/src/MyLib/MyLib.csproj.
  Restore completed in 655.63 ms for /home/nathan/projects/coveralls-netcore/test/MyLib.Tests/MyLib.Tests.csproj.
Finished executing task: Restore

========================================
Build
========================================
Executing task: Build
Microsoft (R) Build Engine version 15.7.179.62826 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

/home/nathan/projects/coveralls-netcore/src/MyLib/MyLib.csproj : warning NU1603: MyLib depends on NETStandard.Library (>= 2.0.2-servicing-26420-0) but NETStandard.Library 2.0.2-servicing-26420-0 was not found. An approximate best match of NETStandard.Library 2.0.2 was resolved.
  MyLib -> /home/nathan/projects/coveralls-netcore/src/MyLib/bin/Release/netstandard2.0/MyLib.dll
  MyLib.Tests -> /home/nathan/projects/coveralls-netcore/test/MyLib.Tests/bin/Release/netcoreapp2.1/MyLib.Tests.dll

Build succeeded.

/home/nathan/projects/coveralls-netcore/src/MyLib/MyLib.csproj : warning NU1603: MyLib depends on NETStandard.Library (>= 2.0.2-servicing-26420-0) but NETStandard.Library 2.0.2-servicing-26420-0 was not found. An approximate best match of NETStandard.Library 2.0.2 was resolved.
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.64
Finished executing task: Build

========================================
Test
========================================
Executing task: Test
Assembly resolver search directories:
/home/nathan/projects/coveralls-netcore/test/MyLib.Tests/obj/Release/netcoreapp2.1

Assembly resolver search directories:
/home/nathan/projects/coveralls-netcore/test/MyLib.Tests/bin/Release/netcoreapp2.1
/home/nathan/.nuget/packages
/opt/dotnet/sdk/NuGetFallbackFolder

Instrumenting assembly "MyLib"
Changing working directory to '/home/nathan/projects/coveralls-netcore'
Reset coverage for directory: '/home/nathan/projects/coveralls-netcore' on pattern './coverage-hits.txt'
Directory is already cleared
Test run for /home/nathan/projects/coveralls-netcore/test/MyLib.Tests/bin/Release/netcoreapp2.1/MyLib.Tests.dll(.NETCoreApp,Version=v2.1)
Microsoft (R) Test Execution Command Line Tool Version 15.3.0-preview-20170628-02
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
[xUnit.net 00:00:00.4964089]   Discovering: MyLib.Tests
[xUnit.net 00:00:00.5609486]   Discovered:  MyLib.Tests
[xUnit.net 00:00:00.5680125]   Starting:    MyLib.Tests
[xUnit.net 00:00:00.7413388]   Finished:    MyLib.Tests

Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 1.6506 Seconds
Changing working directory to '/home/nathan/projects/coveralls-netcore'
+-------------------+-------+---------------+------------+
| File              | Lines | Covered Lines | Percentage |
+-------------------+-------+---------------+------------+
| src/MyLib/Math.cs |    2  |        1      |   50.000%  |
+-------------------+-------+---------------+------------+
| All files         |    2  |        1      |   50.000%  |
+-------------------+-------+---------------+------------+
Finished executing task: Test

Task                          Duration            
--------------------------------------------------
Restore                       00:00:01.9525117    
Build                         00:00:02.9521372    
Test                          00:00:06.5041439    
--------------------------------------------------
Total:                        00:00:11.4087928
```

## Automating Tests & Publishing Coverage

Now that we have a build script to run our tests and generate coverage results, we can connect our repo to [TravisCI](https://docs.travis-ci.com/user/getting-started) and [Coveralls](https://docs.coveralls.io/). Follow the getting started guides to connect both services to your GitHub account and add your repo. Then, create a `.travis.yml` file at the root of your repo with the following contents:

```yml
language: csharp
mono: none
dotnet: 2.1

script:
- ./build/travis.sh
```

I like to wrap my Travis builds in a separate script. This way, if I publish a Docker Container, I can log in to Docker Hub and push the image only on the master branch, which I can detect with environment variables that Travis exposes:

```bash
# ./build/travis.sh


#!/bin/bash
set -euo pipefail

SCRIPT_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
CAKE_TASK=Coveralls

echo "Building task ${CAKE_TASK}"
"${SCRIPT_ROOT}/../build.sh" -t "${CAKE_TASK}"
```

We're also going to add a new Cake task to upload coverage results to Coveralls:

```csharp
Task("Coveralls")
    .IsDependentOn("Test")
    .Does(() => 
{
    if (!TravisCI.IsRunningOnTravisCI)
    {
        Warning("Not running on travis, cannot publish coverage");
        return;
    }

    MiniCoverReport(new MiniCoverSettings()
        .WithCoverallsSettings(c => c.UseTravisDefaults())
        .GenerateReport(ReportType.COVERALLS)
    );
});
```

Coveralls has this lovely integration with Travis, where we only need to provide the build ID and it'll figure out the rest of the commit information automatically. If you aren't building in TravisCI, you will need to specify additional information. See the [`CoverallsSettings`](http://cakebuild.net/api/Cake.MiniCover.Settings/CoverallsSettings/) extension methods for details on how to provide this information via `Cake.MiniCover`.

Now, we have TravisCI automatically [building & testing](https://travis-ci.org/nlowe/coveralls-netcore/builds/406664517) our code, and Coveralls [tracking code coverage](https://coveralls.io/builds/18100397)!

## Conclusion

I hope you found this helpful. I'd love to be able to provide MiniCover support without requiring a tools project, but that requires MiniCover to be split into a core library and a cli tool: {% github https://github.com/lucaslorentz/minicover/issues/31 %}

If you notice any problems with `Cake.MiniCover`, definitely open an [issue](https://github.com/nlowe/cake.minicover)! If you have a feature you'd like to see added, pull requests are welcome as well.