Unit Testing and Code Coverage with Jenkins and .NET Core

Georg Dangl by Georg Dangl in Continuous Integration Monday, May 30, 2016

Monday, May 30, 2016

Posted in Jenkins DotNet

Update 19.08.2017:

There's an updated version of this post that's targeted to the most recent tools and frameworks. It's simplified and uses the current dotnet CLI *.csproj format. 


This post is describing a rather generic way of unit testing .Net Core projects and I've published quite a long script to achieve that. If you're happy with a really short and simple solution, you can read here how to set up basic unit testing for .Net Core and TypeScript.

This post is assuming a Windows environment. Check here for instructions on Linux.

Now that .NET Core RC2 has been released (this tutorial still works in the released versions, for example 1.0 and 1.1), there have been quite a few changes to the tooling around the stack. I've converted a small sample application (available at GitHub) and tried to set up the CI system for continuous testing and deployment. In this article, I'll describe how to perform unit test and code coverage analysis on the new platform.

Setup Required Tools

In order to successfully run unit tests and code coverage analysis for a .NET Core RC2 project, you first have to install the .NET Core Preview 1 SDK. It is available at the official dot.net site and installs the dotnet CLI tool and registers it globally for all users on the PATH variable.

In Jenkins, you'll need the following plugins:

Additionally, add the following three dependencies to the project.json of your unit test project(s): 

"dependencies": {
    "OpenCover": "4.6.261-rc",
    "OpenCoverToCoberturaConverter": "0.2.4",
    "ReportGenerator": "2.4.5"
}
Tip: If you're having trouble with the xUnit test runner, make sure to have at least version 1.0.0-rc2-build10025 of the dotnet-test-xunit package referenced.

OpenCover is the process that wraps around the actual dotnet test runner and collects coverage analysis. OpenCoverToCoberturaConverter  translates the coverage reports into the Cobertura format for which there are Jenkins plugins. Additionally, ReportGenerator is a tool that creates some Html pages to visualize code coverage.

Configure the Job in Jenkins

Create a Execute Windows Batch Command as first build step with the single command dotnet restore, so that all projects within the workspace have their dependencies restored.

Tip: If you're using private NuGet feeds as package sources, the NuGet version that is bundled with the RC2 tooling does not support encrypted passwords but only plain text passwords in a NuGet.config file. See the NuGet documentation for how to setup clear text passwords for private repositories.

The second step is to execute the following PowerShell script (sorry for it being so long=):

$testProjects = @("src\Dangl.WebDocumentation.Tests")                                  # Array of test projects containing the xUnit tests you want to run, relative to the workspace directory
$filterRootNamespace = "Dangl"                                                         # If you only want to get coverage for a specific root namespace, eg. "MyCompany.*" Leave empty otherwise
$reportGeneratorHistoryPath = "C:\BuildCoverageReportHistories\Dangl.WebDocumentation" # Path where the history of the code coverage Html reports are stored - Must be outside the workspace so it does get. Leave empty if no history is wanted
# Modify the values below here if you need to - Standards should be fine
$dotnetPath = "C:\Program Files\dotnet\dotnet.exe"
$jenkinsWorkspace = $ENV:WORKSPACE # Use $ENV:WORKSPACE in Jenkins
$codeCoverageHtmlReportDirectory = "CodeCoverageHtmlReport"
$xUnitResultName = "xUnitResults.testresults"
$openCoverResultPath = Join-Path -Path $jenkinsWorkspace -ChildPath "OpenCoverCoverageReport.coverage"
$coberturaResultPath = Join-Path -Path $jenkinsWorkspace -ChildPath "CoberturaCoverageReport.coberturacoverage"
# Get the most recent ReportGenerator NuGet package from the dotnet nuget packages
$nugetReportGeneratorPackage = Join-Path -Path $env:USERPROFILE -ChildPath "\.nuget\packages\ReportGenerator"
$latestReportGenerator = Join-Path -Path ((Get-ChildItem -Path $nugetReportGeneratorPackage | Sort-Object Fullname -Descending)[0].FullName) -ChildPath "tools\ReportGenerator.exe"
# Get the most recent OpenCover NuGet package from the dotnet nuget packages
$nugetOpenCoverPackage = Join-Path -Path $env:USERPROFILE -ChildPath "\.nuget\packages\OpenCover"
$latestOpenCover = Join-Path -Path ((Get-ChildItem -Path $nugetOpenCoverPackage | Sort-Object Fullname -Descending)[0].FullName) -ChildPath "tools\OpenCover.Console.exe"
# Get the most recent OpenCoverToCoberturaConverter from the dotnet nuget packages
$nugetCoberturaConverterPackage = Join-Path -Path $env:USERPROFILE -ChildPath "\.nuget\packages\OpenCoverToCoberturaConverter"
$latestCoberturaConverter = Join-Path -Path (Get-ChildItem -Path $nugetCoberturaConverterPackage | Sort-Object Fullname -Descending)[0].FullName -ChildPath "tools\OpenCoverToCoberturaConverter.exe"
# Run unit tests with OpenCover attached for each test project
ForEach ($testProject in $testProjects){
    $testProjectPath = Join-Path -Path $jenkinsWorkspace -ChildPath $testProject
    # Create a unique output file name for the xUnit result
    $xUnitOutputCommand = "-xml \""" + (Join-Path -Path $jenkinsWorkspace -ChildPath ([Guid]::NewGuid().ToString() + "_" + $xUnitResultName)) + "\"""
    # Construct OpenCover arguments
    $openCoverArguments = New-Object System.Collections.ArrayList
    [void]$openCoverArguments.Add("-register:user")
    [void]$openCoverArguments.Add("-target:""" + $dotnetPath + """")
    [void]$openCoverArguments.Add("-targetargs:"" test " + "\""" +$testProjectPath + "\project.json\"" " + $xUnitOutputCommand + """") # dnx arguments
    [void]$openCoverArguments.Add("-output:""" + $openCoverResultPath + """") # OpenCover result output
    [void]$openCoverArguments.Add("-returntargetcode") # Force OpenCover to return an errorenous exit code if the xUnit runner returns one
    [void]$openCoverArguments.Add("-mergeoutput") # Needed if there are multiple test projects
    [void]$openCoverArguments.Add("-oldstyle") # Necessary until https://github.com/OpenCover/opencover/issues/595 is resolved
    if(!([System.String]::IsNullOrWhiteSpace($filterRootNamespace))) {
        [void]$openCoverArguments.Add("-filter:""+[" + $filterRootNamespace + "*]*""") # Check only defined namespaces if specified
    }
    # Run OpenCover with the dotnet text command
    "Running OpenCover tests with the dotnet test command"
    $openCoverProcess = Start-Process -FilePath $latestOpenCover -ArgumentList $openCoverArguments -Wait -PassThru -NoNewWindow
}
# Converting coverage reports to Cobertura format
$coberturaConverterArguments = New-Object System.Collections.ArrayList
[void]$coberturaConverterArguments.Add("-input:""" + $openCoverResultPath + """")
[void]$coberturaConverterArguments.Add("-output:""" + $coberturaResultPath + """")
[void]$coberturaConverterArguments.Add("-sources:""" + $jenkinsWorkspace + """")
$coberturaConverterProcess = Start-Process -FilePath $latestCoberturaConverter -ArgumentList $coberturaConverterArguments -Wait -PassThru -NoNewWindow
if ($coberturaConverterProcess.ExitCode -ne 0) {
    "Exiting due to CoberturaToOpenCoverConverter process having returned an error, exit code: " + $coberturaConverterProcess.ExitCode
    exit $coberturaConverterProcess.ExitCode
} else {
    "Finished running CoberturaToOpenCoverConverter"
}
"Creating the Html report for code coverage results"
# Creating the path for the Html code coverage reports
$codeCoverageHtmlReportPath = Join-Path -Path $jenkinsWorkspace -ChildPath $codeCoverageHtmlReportDirectory
if (-Not (Test-Path -Path $codeCoverageHtmlReportPath -PathType Container)) {
    New-Item -ItemType directory -Path $codeCoverageHtmlReportPath | Out-Null
}
# Create arguments to be passed to the ReportGenerator executable
$reportGeneratorArguments = New-Object System.Collections.ArrayList
[void]$reportGeneratorArguments.Add("-reports:""" + $openCoverResultPath + """")
[void]$reportGeneratorArguments.Add("-targetdir:""" + $codeCoverageHtmlReportPath + """")
if(!([System.String]::IsNullOrWhiteSpace($reportGeneratorHistoryPath))) {
    "Using history for ReportGenerator with directory: " + $reportGeneratorHistoryPath
    [void]$reportGeneratorArguments.Add("-historydir:""" + $reportGeneratorHistoryPath + """") # Check only defined namespaces if specified
} else {
    "Not using history for ReportGenerator"
}
# Run ReportGenerator
$reportGeneratorProcess = Start-Process -FilePath $latestReportGenerator -ArgumentList $reportGeneratorArguments -Wait -PassThru -NoNewWindow
if ($reportGeneratorProcess.ExitCode -ne 0) {
    "Exiting due to ReportGenerator process having returned an error, exit code: " + $reportGeneratorProcess.ExitCode
    exit $reportGeneratorProcess.ExitCode
} else {
    "Finished running ReportGenerator"
}
"Finished running unit tests and code coverage"
Attention: Line 33 is currently necessary due to a renaming of the mscrolib.dll in .Net Core

This script does call the OpenCover tool to consecutively run all test projects and aggregates the results. Then, they are converted into the Cobertura format (so that Jenkins can pick them up). Finally, the ReportGenerator tool is called to create an additional visualization of the coverage analysis.

Since the resulting files are being stored in the workspace, you need to either configure the job to clean before each build or add another step to delete all earlier results from previous runs. Otherwise, you might run into trouble with duplicated result data.

Evaluate Results in Jenkins

The following three images show the necessary post build steps to publish the results in Jenkins. The file patterns are actually from the generated files in the PowerShell script above, so if you're modifying the script to use other filenames don't forget to adjust your post build actions!

Jenkins xUnit Result Publishing Configuration

Jenkins Cobertura Code Coverage Analysis Report

Jenkins Custom Html Report Publishing Configuration

View the Results in Jenkins

Now when you run the job, you'll have the following graphs available in your jobs main page:

Jenkins CI Unit Test and Code Coverage Results

There's also an entry called Code Coverage Source Visualization on your left menu in the job overview where you'll see visualized code coverage analysis for your project.

Update 25.09.2016: Added info about how to resolve zero code coverage reports due to rename of mscorlib.dll

Update 31.05.2017: Added link to Linux tutorial

Update 19.08.2017: Added link to updated edition for the current csproj format


Share this post


comments powered by Disqus

Dangl.Blog();
// Just 💗 Coding

Social Links