Введение: Почему тестирование — это не роскошь, а необходимость
В мире современной разработки на C# (.NET) качество кода определяет не только скорость выхода продукта, но и его репутацию. Ошибки на production, внезапные падения после обновления зависимости или некорректная работа бизнес-логики — это сценарии, которых можно избежать с помощью грамотно выстроенного тестирования.
Многие разработчики, особенно начинающие, воспринимают тесты как "лишнюю работу". Однако на практике тестирование экономит часы отладки и рефакторинга. В этой статье мы разберём три ключевых уровня тестирования в экосистеме C#: Unit (модульные), Integration (интеграционные) и E2E (End-to-End). Вы увидите, как каждый из них решает свои задачи, какие инструменты использовать и как писать чистые, поддерживаемые тесты.
1. Unit-тестирование: Фундамент качества
Unit-тесты проверяют изолированные блоки кода (обычно один метод или класс) в отрыве от внешних зависимостей. В C# это самый быстрый и стабильный уровень тестирования. Основная цель — убедиться, что каждая функция работает корректно при различных входных данных.
Инструменты для Unit-тестов в C#
Самые популярные фреймворки для .NET — xUnit (рекомендуется Microsoft) и NUnit. Для подмены зависимостей используется библиотека Moq или встроенный в .NET FakeItEasy.
Пример Unit-теста (xUnit + Moq)
Допустим, у нас есть сервис расчёта скидки, который зависит от репозитория пользователей:
public class DiscountService
{
private readonly IUserRepository _userRepo;
public DiscountService(IUserRepository userRepo)
{
_userRepo = userRepo;
}
public decimal CalculateDiscount(int userId, decimal orderTotal)
{
var user = _userRepo.GetById(userId);
if (user.IsVip)
return orderTotal * 0.2m; // 20% скидка
return orderTotal * 0.05m; // 5% скидка
}
}
Тест для этого метода:
public class DiscountServiceTests
{
[Fact]
public void CalculateDiscount_WhenUserIsVip_Returns20Percent()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(repo => repo.GetById(1))
.Returns(new User { Id = 1, IsVip = true });
var service = new DiscountService(mockRepo.Object);
// Act
var result = service.CalculateDiscount(1, 100m);
// Assert
Assert.Equal(20m, result);
}
}
Важно: В unit-тестах мы не обращаемся к реальной базе данных. Мы используем mock, чтобы изолировать логику DiscountService.
2. Интеграционное тестирование: Проверка связки компонентов
Интеграционные тесты проверяют взаимодействие нескольких реальных компонентов: базы данных, API, файловой системы. В .NET для этого часто используют WebApplicationFactory (для ASP.NET Core) и тестовые контейнеры (Testcontainers).
Особенности Integration-тестов
- Медленнее unit-тестов — требуют поднятия инфраструктуры (БД, HTTP-сервер).
- Дороже в поддержке — нужно управлять состоянием тестовой среды.
- Ценность — выявляют проблемы в конфигурации, маппинге данных, работе с реальной БД.
Пример Integration-теста (ASP.NET Core + xUnit)
Предположим, у нас есть контроллер для работы с заказами. Тест будет проверять, что запрос к реальному API возвращает корректный JSON:
public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public OrdersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetOrders_ReturnsSuccessStatusCode()
{
// Act
var response = await _client.GetAsync("/api/orders");
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("orderId", content);
}
}
Совет: Для изоляции тестов используйте отдельную тестовую базу данных (например, SQLite in-memory или PostgreSQL в Docker через Testcontainers).
3. E2E-тестирование: Сквозная проверка пользовательского сценария
End-to-End тесты симулируют действия реального пользователя: открытие браузера, клики, заполнение форм. В экосистеме C# для этого чаще всего используют Selenium WebDriver или Playwright для .NET.
Когда нужны E2E-тесты?
- Проверка критических бизнес-сценариев (регистрация, оплата, создание заказа).
- Тестирование UI-компонентов и вёрстки.
- Валидация работы после деплоя на стейджинг.
Пример E2E-теста (Selenium + xUnit)
Тест проверяет, что пользователь может войти в систему:
public class LoginE2ETests
{
[Fact]
public void Login_WithValidCredentials_RedirectsToDashboard()
{
using var driver = new ChromeDriver();
driver.Navigate().GoToUrl("https://test-app.example.com/login");
driver.FindElement(By.Id("username")).SendKeys("testuser");
driver.FindElement(By.Id("password")).SendKeys("Test@123");
driver.FindElement(By.Id("login-btn")).Click();
Assert.Contains("dashboard", driver.Url);
}
}
Важно: E2E-тесты самые медленные и хрупкие (зависят от стабильности UI, сети). Их должно быть не более 5-10% от общего количества тестов в проекте.
Заключение: Как выстроить стратегию тестирования в C#
Идеальная пирамида тестирования для .NET-проекта выглядит так:
- 70% Unit-тестов — быстрые, покрывают бизнес-логику.
- 20% Integration-тестов — проверяют работу с БД и внешними API.
- 10% E2E-тестов — только критические сценарии.
Не пытайтесь покрыть тестами всё сразу. Начните