Code coverage measures which lines, branches, and methods are executed during tests. It is a useful tool for finding gaps in the test suite, not a quality metric in itself — 90% coverage with meaningless assertions provides less value than 70% coverage with focused, meaningful tests. The goal is to use coverage reports to identify genuinely untested critical paths, then add tests that make meaningful assertions about those paths.
Coverage Configuration and Reporting
// ── Run tests with coverage collection ────────────────────────────────────
// dotnet test --collect:"XPlat Code Coverage" --results-directory ./coverage
// ── Install ReportGenerator ────────────────────────────────────────────────
// dotnet tool install -g dotnet-reportgenerator-globaltool
//
// Generate HTML report from coverage XML:
// reportgenerator
// -reports:"./coverage/**/coverage.cobertura.xml"
// -targetdir:"./coverage/report"
// -reporttypes:"Html;Cobertura;lcov"
// -assemblyfilters:"-*Tests*;-*Migrations*"
// -filefilters:"-*Generated*;-*Scaffold*"
// -classfilters:"-*Dto;-*ViewModel;-*Exception"
// ── Coverlet configuration in csproj ─────────────────────────────────────
// BlogApp.Tests.csproj:
// <PropertyGroup>
// <CollectCoverage>true</CollectCoverage>
// <CoverletOutputFormat>cobertura</CoverletOutputFormat>
// <CoverletOutput>./coverage/</CoverletOutput>
// <Exclude>[*]BlogApp.Api.Migrations.*;[*]*Dto;[*]*Exception</Exclude>
// <ExcludeByAttribute>GeneratedCodeAttribute;CompilerGeneratedAttribute</ExcludeByAttribute>
// <Threshold>80</Threshold>
// <ThresholdType>line</ThresholdType>
// <ThresholdStat>minimum</ThresholdStat>
// </PropertyGroup>
// ── Combine unit + integration coverage ───────────────────────────────────
// Run both test projects and merge:
// dotnet test BlogApp.UnitTests --collect:"XPlat Code Coverage" --results-directory ./cov/unit
// dotnet test BlogApp.IntegTests --collect:"XPlat Code Coverage" --results-directory ./cov/int
// reportgenerator
// -reports:"./cov/**/coverage.cobertura.xml" -- merges all XML files
// -targetdir:"./cov/merged"
// -reporttypes:"Html;lcov"
// ── Reading the coverage report ───────────────────────────────────────────
// GREEN (covered): all branches of an if/else were executed during tests
// YELLOW (partial): some branches not executed (partial branch coverage)
// RED (uncovered): line never executed by any test
//
// High-value targets to test (currently uncovered):
// - PostsService.PublishAsync — error paths (wrong status, wrong owner)
// - AuthService.LoginAsync — account lockout path
// - FileUploadController.UploadImage — invalid magic bytes path
// - GlobalExceptionHandler — for each exception type mapping
//
// Low-value to skip (typically excluded):
// - EF Core migrations (auto-generated)
// - DTOs/ViewModels (no logic)
// - Exception subclasses (just constructors)
// - Program.cs bootstrapping code
return post.Status == "review" ? PublishPost() : throw new DomainException() is one line but has two branches. Line coverage says “this line was executed” even if only one branch was taken. Branch coverage reveals that the unhappy path (the throw) was never tested. Always check both line and branch coverage for service-layer code with conditional logic.ExcludeByAttribute setting to automatically exclude auto-generated code marked with [GeneratedCode] and [CompilerGenerated] attributes — these come from source generators, EF Core scaffolding, and C# compiler-generated code (async state machines, record members). Excluding them prevents inflated “uncovered” percentages for code you cannot and should not test directly, and keeps the coverage report focused on your actual business code.Coverage Metrics Reference
| Metric | What It Measures | Best For |
|---|---|---|
| Line coverage | % of lines executed | Quick health check |
| Branch coverage | % of if/else/switch branches taken | Business logic paths |
| Method coverage | % of methods called | Finding untested features |
| Statement coverage | % of statements executed | Similar to line, more granular |
Common Mistakes
Mistake 1 — Measuring coverage only from unit tests (misses integration-tested paths)
❌ Wrong — only unit test coverage; many controller and middleware paths only covered by integration tests; report shows false low numbers.
✅ Correct — merge coverage from unit tests AND integration tests; the combined report reflects total real coverage.
Mistake 2 — Setting 100% threshold (blocks all PRs with new untested infrastructure code)
❌ Wrong — 100% threshold; developer adds a new logging statement; line not tested; PR blocked.
✅ Correct — 75-85% threshold with reasonable exclusions; focus on testing business logic, not infrastructure boilerplate.