Тестирование в C#: от Unit до E2E — как построить качественную архитектуру проверок в .NET
Качество программного обеспечения напрямую зависит от системы тестирования. В экосистеме C# и .NET существует устоявшаяся пирамида тестирования: модульные (unit), интеграционные (integration) и сквозные (end-to-end) тесты. В этой статье мы разберём каждый уровень на практических примерах, покажем, какие библиотеки использовать, и дадим готовые шаблоны кода.
1. Unit-тестирование: основа стабильности кода
Unit-тесты проверяют работу отдельных методов, классов или компонентов изолированно от внешних зависимостей (БД, API, файловой системы). Основная цель — быстро найти регрессию при рефакторинге.
Инструменты для C#
- NUnit — классический фреймворк с атрибутами
[Test],[TestCase]. - xUnit — современный фреймворк от создателей NUnit, предпочтителен в новых проектах.
- Moq — библиотека для создания mock-объектов.
- FluentAssertions — для читаемых assert-ов.
Пример unit-теста на xUnit с Moq
Допустим, у нас есть сервис расчёта скидки, который обращается к репозиторию пользователей:
public interface IUserRepository
{
bool IsPremiumUser(int userId);
}
public class DiscountService
{
private readonly IUserRepository _userRepo;
public DiscountService(IUserRepository userRepo) => _userRepo = userRepo;
public decimal CalculateDiscount(int userId, decimal amount)
{
if (_userRepo.IsPremiumUser(userId))
return amount * 0.9m; // 10% скидка
return amount;
}
}
// Тест
public class DiscountServiceTests
{
[Fact]
public void CalculateDiscount_ForPremiumUser_Returns90Percent()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(r => r.IsPremiumUser(It.IsAny<int>())).Returns(true);
var service = new DiscountService(mockRepo.Object);
// Act
var result = service.CalculateDiscount(1, 100m);
// Assert
Assert.Equal(90m, result);
}
}
Совет: Используйте [Theory] и [InlineData] в xUnit для параметризованных тестов — это сокращает дублирование кода.
2. Интеграционное тестирование: проверка связки компонентов
Интеграционные тесты проверяют, как ваши модули взаимодействуют с реальными внешними системами: базами данных, очередями, веб-сервисами. В отличие от unit-тестов, здесь не используются mock-и — мы поднимаем настоящие окружения.
Библиотеки и подходы
- TestContainers — запускает Docker-контейнеры (PostgreSQL, SQL Server, Redis) прямо из тестов.
- WebApplicationFactory (из Microsoft.AspNetCore.Mvc.Testing) — создаёт тестовый хост ASP.NET Core.
- Respawn — очищает БД между тестами, не пересоздавая контейнер.
Пример интеграционного теста с TestContainers и WebApplicationFactory
Проверим, что наш API корректно сохраняет пользователя в PostgreSQL:
public class UserApiIntegrationTests : IClassFixture<TestContainersFixture>
{
private readonly HttpClient _client;
private readonly TestContainersFixture _fixture;
public UserApiIntegrationTests(TestContainersFixture fixture)
{
_fixture = fixture;
var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
// Заменяем реальную строку подключения на контейнер
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(fixture.ConnectionString));
});
});
_client = factory.CreateClient();
}
[Fact]
public async Task CreateUser_Returns201()
{
// Act
var response = await _client.PostAsJsonAsync("/api/users", new { Name = "Test" });
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
}
// Фикстура для TestContainers
public class TestContainersFixture : IAsyncLifetime
{
private readonly PostgreSqlContainer _container = new PostgreSqlBuilder().Build();
public string ConnectionString => _container.GetConnectionString();
public async Task InitializeAsync() => await _container.StartAsync();
public async Task DisposeAsync() => await _container.DisposeAsync();
}
Важно: Интеграционные тесты медленнее unit-тестов, поэтому их количество должно быть разумным — покрывайте только критические сценарии взаимодействия.
3. E2E (End-to-End) тестирование: проверка пользовательского пути
E2E-тесты имитируют действия реального пользователя: нажатие кнопок, заполнение форм, переходы между страницами. Они запускаются на полностью собранном приложении (frontend + backend + БД). Для C# веб-приложений популярен связка Selenium WebDriver или Playwright для .NET.
Пример E2E-теста с Selenium WebDriver
Проверим, что после логина пользователь видит приветствие:
public class LoginE2ETests : IDisposable
{
private readonly IWebDriver _driver;
public LoginE2ETests()
{
_driver = new ChromeDriver(); // требуется ChromeDriver.exe
_driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
[Fact]
public void Login_WithValidCredentials_ShowsWelcome()
{
// Arrange
_driver.Navigate().GoToUrl("https://localhost:5001/login");
_driver.FindElement(By.Id("Email")).SendKeys("user@test.com");
_driver.FindElement(By.Id("Password")).SendKeys("Test123!");
_driver.FindElement(By.Id("submit")).Click();
// Assert
var welcomeElement = _driver.FindElement(By.ClassName("welcome-message"));
Assert.Contains("Добро пожаловать", welcomeElement.Text);
}
public void Dispose() => _driver.Quit();
}
Альтернатива: Используйте Playwright — он быстрее Selenium и поддерживает все современные браузеры. Установите пакет Microsoft.Playwright.NUnit.
Лучшие практики E2E
- Не тестируйте то, что уже покрыто unit/integration тестами (например, валидацию модели).