Skip to main content

.NET Code Coverage

Introduction

Code coverage is a critical metric in software testing that measures the extent to which your source code is tested by your test suite. It helps identify which parts of your code are executed during testing and which parts remain untested. In the .NET ecosystem, code coverage tools help developers ensure their tests adequately cover the application code, improving software quality and reducing the risk of undiscovered bugs.

In this guide, we'll explore:

  • What code coverage is and why it matters
  • Different types of code coverage metrics
  • Tools available for .NET code coverage analysis
  • How to set up and run code coverage in a .NET project
  • How to interpret and improve code coverage results

What is Code Coverage?

Code coverage is a measurement of how much of your application code is executed when your automated tests run. It's expressed as a percentage, with higher percentages indicating that more code is being tested. While 100% code coverage doesn't guarantee bug-free code, low code coverage often indicates insufficient testing.

Why Code Coverage Matters

  • Identifies untested code: Reveals parts of your application that aren't being tested
  • Guides test development: Helps you focus on writing tests for uncovered code
  • Quality metric: Provides a quantifiable measure of test thoroughness
  • Regression safety: Higher coverage typically means less risk when making changes

Types of Code Coverage Metrics

Several types of code coverage metrics exist, each measuring different aspects of your code:

1. Line Coverage

The most basic form, measuring what percentage of code lines were executed.

2. Branch Coverage

Measures whether each possible branch from decision points (like if statements) has been executed.

3. Method Coverage

Tracks whether each method in your codebase has been called during testing.

4. Statement Coverage

Similar to line coverage, but counts individual statements rather than lines.

5. Condition Coverage

Evaluates whether each boolean subexpression has been evaluated as both true and false.

.NET Code Coverage Tools

Several tools are available for measuring code coverage in .NET applications:

1. Visual Studio Enterprise

Microsoft's premium Visual Studio edition includes built-in code coverage tools.

2. Coverlet

An open-source cross-platform code coverage framework for .NET.

3. ReportGenerator

A tool that converts coverage reports into human-readable formats.

4. dotCover by JetBrains

A commercial tool from JetBrains with ReSharper integration.

5. Azure DevOps

Built-in code coverage reporting for CI/CD pipelines.

Setting Up Code Coverage in a .NET Project

Let's focus on Coverlet, which is the most popular open-source solution for .NET code coverage.

Step 1: Add Coverlet to Your Test Project

First, add the Coverlet packages to your test project:

bash
dotnet add package coverlet.collector
dotnet add package coverlet.msbuild

Step 2: Configure Coverage Collection

You can configure Coverlet either through the command line or by adding settings to your test project file:

Add this to your test project's .csproj file:

xml
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<CoverletOutput>./TestResults/Coverage/</CoverletOutput>
</PropertyGroup>

Step 3: Run Tests with Coverage

Run your tests with coverage enabled:

bash
dotnet test --collect:"XPlat Code Coverage"

Or if you've modified the project file:

bash
dotnet test

Step 4: Generate a Human-Readable Report

To convert the coverage data into a readable format, you'll need ReportGenerator:

bash
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:"./TestResults/Coverage/coverage.cobertura.xml" -targetdir:"./CoverageReport" -reporttypes:Html

Real-World Example

Let's go through a complete example with a simple calculator application.

1. The Code Being Tested

Here's a simple Calculator class:

csharp
namespace CalculatorApp
{
public class Calculator
{
public int Add(int a, int b) => a + b;

public int Subtract(int a, int b) => a - b;

public int Multiply(int a, int b) => a * b;

public int Divide(int a, int b)
{
if (b == 0)
throw new DivideByZeroException("Cannot divide by zero");

return a / b;
}

public bool IsEven(int number) => number % 2 == 0;
}
}

2. Incomplete Tests

Here's a test class that doesn't cover all methods:

csharp
using Xunit;
using CalculatorApp;

namespace CalculatorApp.Tests
{
public class CalculatorTests
{
private readonly Calculator _calculator = new Calculator();

[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
// Arrange
int a = 5, b = 7;

// Act
int result = _calculator.Add(a, b);

// Assert
Assert.Equal(12, result);
}

[Fact]
public void Subtract_TwoNumbers_ReturnsDifference()
{
// Arrange
int a = 10, b = 4;

// Act
int result = _calculator.Subtract(a, b);

// Assert
Assert.Equal(6, result);
}
}
}

3. Running Coverage Analysis

When we run code coverage on these tests:

bash
dotnet test --collect:"XPlat Code Coverage"

4. Coverage Results

The report would show:

  • Add method: 100% covered
  • Subtract method: 100% covered
  • Multiply method: 0% covered
  • Divide method: 0% covered
  • IsEven method: 0% covered

This tells us we need to write tests for the Multiply, Divide, and IsEven methods.

5. Improving Coverage

Let's add tests for the uncovered methods:

csharp
[Fact]
public void Multiply_TwoNumbers_ReturnsProduct()
{
// Arrange
int a = 3, b = 4;

// Act
int result = _calculator.Multiply(a, b);

// Assert
Assert.Equal(12, result);
}

[Fact]
public void Divide_TwoNumbers_ReturnsQuotient()
{
// Arrange
int a = 20, b = 5;

// Act
int result = _calculator.Divide(a, b);

// Assert
Assert.Equal(4, result);
}

[Fact]
public void Divide_ByZero_ThrowsException()
{
// Arrange
int a = 10, b = 0;

// Act & Assert
Assert.Throws<DivideByZeroException>(() => _calculator.Divide(a, b));
}

[Theory]
[InlineData(2, true)]
[InlineData(3, false)]
public void IsEven_Number_ReturnsCorrectResult(int number, bool expected)
{
// Act
bool result = _calculator.IsEven(number);

// Assert
Assert.Equal(expected, result);
}

After adding these tests and re-running coverage, we'd achieve 100% code coverage for our calculator.

Code Coverage in CI/CD Pipelines

Integrating code coverage into your CI/CD pipeline helps maintain or improve coverage as your codebase grows.

GitHub Actions Example

Here's a GitHub Actions workflow that includes code coverage:

yaml
name: .NET Build and Test

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 7.0.x

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore

- name: Test with Coverage
run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"

- name: Generate Coverage Report
uses: danielpalme/ReportGenerator-GitHub-[email protected]
with:
reports: '**/coverage.cobertura.xml'
targetdir: 'coveragereport'
reporttypes: 'HtmlInline;Cobertura'

- name: Upload Coverage Report
uses: actions/upload-artifact@v2
with:
name: coverage-report
path: coveragereport

Best Practices for Code Coverage

  1. Set realistic coverage goals: Aim for 70-80% coverage initially instead of demanding 100%
  2. Focus on critical paths: Ensure business-critical code has higher coverage
  3. Don't chase the number: Don't write meaningless tests just to increase coverage
  4. Combine with other metrics: Use coverage alongside code reviews and other quality measures
  5. Update tests when code changes: Maintain coverage when refactoring or adding features
  6. Include coverage thresholds: Fail builds if coverage drops below a certain percentage

Interpreting Coverage Results

When analyzing your coverage report, look beyond the overall percentage:

  1. Complexity vs. coverage: Focus more on complex methods with low coverage
  2. Branch coverage: Look for untested decision paths
  3. Trends over time: Watch for declining coverage as a warning sign
  4. Edge cases: Check if exception paths are covered

Limitations of Code Coverage

While valuable, code coverage has limitations:

  1. High coverage doesn't guarantee quality tests
  2. Coverage doesn't measure test effectiveness
  3. Some code may be difficult or unnecessary to test
  4. Some bugs occur from interactions not measurable by coverage
  5. Over-focusing on coverage can lead to poor quality tests

Summary

Code coverage is an essential tool in a .NET developer's quality assurance arsenal. It provides quantifiable insights into your test suite's thoroughness and helps identify untested code that may contain bugs. By integrating tools like Coverlet into your development workflow, you can systematically improve your test coverage and build more robust .NET applications.

Remember that while high code coverage is desirable, it's just one aspect of software quality. Combining good coverage with well-designed tests, thorough code reviews, and other quality practices will result in the most reliable applications.

Additional Resources

Exercises

  1. Add code coverage to an existing .NET project and identify areas with low coverage.
  2. Create a GitHub Actions workflow that fails if code coverage drops below a certain threshold.
  3. Generate a code coverage report and analyze which parts of your code need additional tests.
  4. Compare line coverage and branch coverage in a complex method to understand the difference.
  5. Set up a code coverage gate in Azure DevOps that prevents merges when coverage decreases.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)