Почему «зелёные» тесты — это ещё не победа?
Представьте: вы написали сотню юнит-тестов, все они проходят с зелёной галочкой, CI-пайплайн счастлив, менеджер доволен. Но через месяц в продакшене падает критический функционал. Знакомая история? Чаще всего проблема в том, что мы тестируем неправильные сценарии или пропускаем целые ветки кода.
Покрытие кода тестами (code coverage) — это не просто модная метрика для отчёта. Это ваш фонарик в тёмной комнате неопределённости. Когда я начинал работать с C#, то думал, что 80% покрытия — это идеал. Но потом понял: можно иметь 100% покрытие строк и при этом пропустить баг в логике ветвления. Поэтому сегодня мы разберёмся, как правильно измерять покрытие в C#, какие инструменты реально помогают, а какие метрики стоит отслеживать.
Что такое покрытие кода и почему оно важно для C# разработчика?
Говоря просто, покрытие кода показывает, какой процент исходного кода вашего приложения был выполнен во время прогона тестов. Если вы написали тест, который вызывает метод CalculateDiscount(), но не проверяет ветку if (isPremiumUser), то эта ветка остаётся «непокрытой».
Для C#-разработчика покрытие — это индикатор двух вещей:
- Доверие к рефакторингу — можно ли менять код, не боясь что-то сломать?
- Полнота тестов — не забыли ли мы про граничные случаи?
Но есть важный нюанс: код покрытия тестами в C# не гарантирует отсутствие багов. Он лишь говорит, что определённые строки были выполнены. Именно поэтому мы смотрим не просто на процент, а на конкретные метрики.
Основные метрики покрытия кода в .NET
Когда вы запускаете анализ покрытия в C#, вы сталкиваетесь с несколькими видами метрик. Давайте разберём каждую на примере реального кода.
Line Coverage (покрытие строк)
Самая простая метрика. Считает, сколько строк кода было затронуто тестами. Пример:
public int Add(int a, int b)
{
if (a < 0) // строка 1
return 0; // строка 2
return a + b; // строка 3
}
Если ваш тест вызывает Add(5, 3), то будут выполнены строки 1 и 3. Строка 2 останется непокрытой. Line coverage покажет 66% (2 из 3 строк).
Branch Coverage (покрытие ветвлений)
Более честная метрика. Она учитывает все возможные пути выполнения. В нашем примере есть два пути: когда a < 0 и когда нет. Если вы проверили только случай с положительным числом, Branch coverage будет 50%.
Именно Branch coverage часто выявляет «дыры» в тестах. Я рекомендую стремиться к 80%+ именно по этой метрике.
Cyclomatic Complexity и его связь с покрытием
Хотя это не прямая метрика покрытия, циклическая сложность показывает, сколько тестов нужно написать для полного покрытия. Чем выше сложность метода, тем больше тестов требуется. Если видите метод с цикломатической сложностью 15, а покрытие 90% — скорее всего, вы тестируете не все сценарии.
Инструменты для измерения покрытия кода в C#
В экосистеме .NET есть несколько мощных инструментов. Я расскажу о трёх самых популярных, с которыми работал сам.
Coverlet — бесплатный и встроенный в .NET
Если вы используете .NET 6+, то Coverlet уже доступен через NuGet. Это мой любимый инструмент для CI/CD, потому что он не требует установки дополнительных программ.
Установка:
dotnet add package coverlet.msbuild
dotnet add package coverlet.collector
Запуск с отчётом:
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
Вы получите файл coverage.opencover.xml, который можно визуализировать с помощью ReportGenerator:
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:coverage.opencover.xml -targetdir:coveragereport
Coverlet поддерживает Line, Branch и Method coverage. Идеально подходит для небольших и средних проектов.
JetBrains dotCover — мощный, но платный
Если вы используете ReSharper или Rider, dotCover встроен прямо в IDE. Он показывает покрытие в реальном времени — вы запускаете тесты и видите, какие строки выделены зелёным (покрыто) или красным (не покрыто).
Плюсы:
- Интеграция с Visual Studio и Rider
- Детальный анализ по каждому методу
- Поддержка Statement, Branch и Condition coverage
Минусы:
- Лицензия стоит денег
- Тяжеловат для больших решений
Лично я использую dotCover для локальной разработки, а Coverlet — для CI/CD. Идеальное сочетание.
OpenCover — старый, но надёжный
Раньше OpenCover был стандартом в .NET Framework. Сейчас он уступает Coverlet по скорости, но всё ещё работает. Если у вас легаси-проект на .NET Framework, OpenCover — ваш выбор.
Пример команды:
OpenCover.Console.exe -target:"nunit3-console.exe" -targetargs:"MyProject.Tests.dll" -output:coverage.xml
Главный недостаток — нет поддержки .NET Core без дополнительных настроек.
Практический пример: пишем тесты и смотрим покрытие
Давайте представим, что у нас есть сервис расчёта скидки. Напишем код, а затем тесты и посмотрим, как меняется покрытие.
Исходный код (DiscountService.cs)
public class DiscountService
{
public decimal CalculateDiscount(decimal total, bool isLoyaltyMember)
{
if (total < 0)
throw new ArgumentException("Сумма не может быть отрицательной");
decimal discount = 0;
if (total > 1000)
discount = 0.1m;
if (isLoyaltyMember)
discount += 0.05m;
return total * (1 - discount);
}
}
Тесты (только основные сценарии)
[Test]
public void CalculateDiscount_TotalAbove1000_ReturnsDiscountedPrice()
{
var service = new DiscountService();
var result = service.CalculateDiscount(2000, false);
Assert.AreEqual(1800, result);
}
[Test]
public void CalculateDiscount_LoyaltyMember_AddsExtraDiscount()
{
var service = new DiscountService();
var result = service.CalculateDiscount(500, true);
Assert.AreEqual(475, result);
}
Запускаем Coverlet:
dotnet test /p:CollectCoverage=true