Введение: Зачем нужно тестирование в C#?
В современной разработке на C# (.NET) качество кода — это не роскошь, а necessity. Тестирование позволяет не только отлавливать баги до попадания в продакшн, но и служит живой документацией, а также ускоряет рефакторинг. Существует три основных уровня тестирования, которые формируют «пирамиду тестов»: Unit (модульные), Integration (интеграционные) и E2E (сквозные). В этой статье мы разберем каждый из них на примерах реального кода на C#.
1. Unit-тестирование: Фундамент качества
Unit-тесты проверяют изолированный фрагмент кода (метод, класс) в отрыве от внешних зависимостей (базы данных, файловой системы, API). В экосистеме .NET популярны три фреймворка: xUnit (рекомендуется для новых проектов), NUnit и MSTest.
Основные принципы (FIRST)
- Fast — быстрые (миллисекунды).
- Isolated — изолированные (используем моки).
- Repeatable — воспроизводимые.
- Self-validating — с самопроверкой (Assert).
- Timely — своевременные.
Пример Unit-теста с xUnit и Moq
Допустим, у нас есть сервис для расчета скидки:
public interface IDiscountService
{
decimal CalculateDiscount(decimal price, bool isLoyaltyMember);
}
public class DiscountService : IDiscountService
{
public decimal CalculateDiscount(decimal price, bool isLoyaltyMember)
{
if (isLoyaltyMember) return price * 0.9m; // 10% скидка
return price;
}
}
Тест с использованием Moq (для имитации зависимостей) и xUnit:
using Xunit;
using Moq;
public class DiscountServiceTests
{
[Fact]
public void CalculateDiscount_WhenLoyaltyMember_ReturnsDiscountedPrice()
{
// Arrange
var service = new DiscountService();
var price = 100m;
// Act
var result = service.CalculateDiscount(price, true);
// Assert
Assert.Equal(90m, result);
}
[Theory]
[InlineData(100, false, 100)]
[InlineData(200, true, 180)]
public void CalculateDiscount_MultipleCases_ReturnsCorrectValues(decimal price, bool isLoyalty, decimal expected)
{
var service = new DiscountService();
var result = service.CalculateDiscount(price, isLoyalty);
Assert.Equal(expected, result);
}
}
Совет: Используйте атрибут [Fact] для одиночных тестов и [Theory] + [InlineData] для параметризованных.
2. Интеграционное тестирование: Проверка связки компонентов
Интеграционные тесты проверяют, как несколько модулей работают вместе — например, контроллер, сервис и реальная база данных (или её замена, например, in-memory SQLite). В ASP.NET Core для этого используется WebApplicationFactory.
Настройка WebApplicationFactory
Создадим тестовый класс, который поднимает тестовый веб-хост:
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MyApp.Data;
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Удаляем реальный DbContext
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null) services.Remove(descriptor);
// Добавляем in-memory базу
services.AddDbContext<AppDbContext>(options =>
{
options.UseInMemoryDatabase("TestDb");
});
});
}
}
Теперь напишем тест для эндпоинта API:
public class ProductsApiTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
public ProductsApiTests(CustomWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetProducts_ReturnsOkAndList()
{
// Act
var response = await _client.GetAsync("/api/products");
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("product", content.ToLower());
}
}
Важно: Интеграционные тесты медленнее unit-тестов, поэтому их количество должно быть меньше. Они проверяют «склейку» между слоями.
3. E2E-тестирование: Симуляция пользователя
Сквозные (End-to-End) тесты имитируют действия реального пользователя в браузере. Для C# самым популярным инструментом является Selenium WebDriver (или более современный Playwright).
Пример E2E теста с Selenium
Установите пакет Selenium.WebDriver и Selenium.WebDriver.ChromeDriver. Сценарий: открыть страницу логина, ввести данные и проверить, что пользователь перенаправлен на главную.
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using Xunit;
public class LoginE2ETests
{
[Fact]
public void Login_WithValidCredentials_RedirectsToDashboard()
{
using var driver = new ChromeDriver();
try
{
// Arrange
driver.Navigate().GoToUrl("https://localhost:5001/login");
driver.FindElement(By.Id("Email")).SendKeys("user@test.com");
driver.FindElement(By.Id("Password")).SendKeys("Password123!");
driver.FindElement(By.CssSelector("button[type='submit']")).Click();
// Assert
Assert.Contains("dashboard", driver.Url.ToLower());
}
finally
{
driver.Quit();
}
}
}
Рекомендация: Для надежности используйте явные ожидания (WebDriverWait), чтобы избежать ложных падений из-за асинхронности UI.
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
wait.Until(d => d.Url.Contains("dashboard"));
Заключение: Строим сбалансированную стратегию
Идеальная стратегия тестирования в C# — это пирамида: 70% unit-тестов (быстрые и дешевые), 20% интеграционных (провер