Тестирование в C#: Unit, Integration, E2E от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему тестирование — это не роскошь, а необходимость

В мире современной разработки на 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-тестов — только критические сценарии.

Не пытайтесь покрыть тестами всё сразу. Начните

Блоги

Рекомендаций по книгам