Тестирование в C#: Unit, Integration, E2E — продвинутый уровень

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

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

Начать курс

Введение: Зачем писать тесты на продвинутом уровне?

Начальный уровень тестирования в C# обычно ограничивается простыми Assert.Equal и изолированными юнит-тестами. Однако в реальных enterprise-проектах этого недостаточно. Баги часто возникают на стыке компонентов — при взаимодействии с базой данных, внешними API или в сложной бизнес-логике с побочными эффектами.

Продвинутое тестирование в C# (.NET) — это умение строить пирамиду тестов, где юнит-тесты быстрые и изолированные, интеграционные проверяют реальные взаимодействия, а E2E тесты имитируют поведение пользователя. В этой статье мы рассмотрим современные инструменты: xUnit, Moq, FluentAssertions, Testcontainers и WebApplicationFactory. Вы узнаете, как писать надежные тесты, которые не ломаются при рефакторинге и дают уверенность в коде.

1. Продвинутые Unit-тесты: Моки и Stub-объекты

Unit-тесты должны быть изолированы от внешних зависимостей. На продвинутом уровне мы используем библиотеки для создания mock-объектов, такие как Moq или NSubstitute.

Правильная настройка Mock-объектов

Вместо того чтобы создавать заглушки вручную, используйте Mock.Of<T>() для быстрой настройки. Пример с Moq и FluentAssertions:

public interface IEmailService
{
    Task SendAsync(string to, string subject);
}

public class NotificationService
{
    private readonly IEmailService _emailService;

    public NotificationService(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public async Task NotifyUserAsync(string email)
    {
        await _emailService.SendAsync(email, "Welcome!");
    }
}

// Тест
[Fact]
public async Task NotifyUserAsync_ShouldSendEmail()
{
    // Arrange
    var mockEmailService = new Mock<IEmailService>();
    var service = new NotificationService(mockEmailService.Object);

    // Act
    await service.NotifyUserAsync("test@example.com");

    // Assert
    mockEmailService.Verify(
        x => x.SendAsync("test@example.com", "Welcome!"),
        Times.Once);
}

Избегайте тестирования деталей реализации

Продвинутый подход — тестировать поведение, а не внутренние вызовы. Используйте Verify только для критически важных зависимостей. Для остальных — проверяйте результат.

2. Интеграционное тестирование: База данных и внешние сервисы

Интеграционные тесты проверяют, что несколько модулей работают вместе. Для .NET Core/5+ идеальным инструментом является WebApplicationFactory (для ASP.NET Core) и Testcontainers (для контейнеризации зависимостей).

Настройка WebApplicationFactory с реальной БД

Вместо использования InMemory-провайдера (который ведет себя иначе, чем реальная БД), используйте Testcontainers для запуска PostgreSQL или SQL Server в Docker:

public class IntegrationTestFixture : IAsyncLifetime
{
    private readonly TestcontainersContainer _dbContainer;

    public string ConnectionString { get; private set; }

    public IntegrationTestFixture()
    {
        _dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
            .WithImage("postgres:15")
            .WithPortBinding(5432, true)
            .WithEnvironment("POSTGRES_USER", "test")
            .WithEnvironment("POSTGRES_PASSWORD", "test")
            .WithEnvironment("POSTGRES_DB", "testdb")
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();
        ConnectionString = $"Host=localhost;Port={_dbContainer.GetMappedPublicPort(5432)};" +
                           $"Database=testdb;Username=test;Password=test";
    }

    public async Task DisposeAsync()
    {
        await _dbContainer.StopAsync();
    }
}

[CollectionDefinition("Integration Tests")]
public class IntegrationTestCollection : ICollectionFixture<IntegrationTestFixture> { }

Тестирование репозитория

[Collection("Integration Tests")]
public class UserRepositoryTests
{
    private readonly IntegrationTestFixture _fixture;

    public UserRepositoryTests(IntegrationTestFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task CreateUser_ShouldPersistToDatabase()
    {
        // Arrange
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseNpgsql(_fixture.ConnectionString)
            .Options;
        var context = new AppDbContext(options);
        await context.Database.EnsureCreatedAsync();
        var repo = new UserRepository(context);

        // Act
        var user = new User { Name = "Test", Email = "test@test.com" };
        await repo.AddAsync(user);
        var result = await repo.GetByIdAsync(user.Id);

        // Assert
        result.Should().NotBeNull();
        result.Email.Should().Be("test@test.com");
    }
}

3. E2E тестирование: Имитация пользовательского сценария

End-to-End тесты проверяют всю систему целиком. Для веб-приложений на ASP.NET Core используйте Playwright или Selenium, но в контексте .NET удобнее Playwright с его асинхронным C# API.

Настройка Playwright в .NET

public class E2ETests : IAsyncLifetime
{
    private IPlaywright _playwright;
    private IBrowser _browser;
    private IPage _page;

    public async Task InitializeAsync()
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
        {
            Headless = true
        });
        _page = await _browser.NewPageAsync();
    }

    [Fact]
    public async Task UserCanRegisterAndLogin()
    {
        // Arrange
        await _page.GotoAsync("https://localhost:5001/register");

        // Act - заполняем форму
        await _page.FillAsync("input[name='Email']", "user@example.com");
        await _page.FillAsync("input[name='Password']", "Str0ng!Pass");
        await _page.ClickAsync("button[type='submit']");

        // Assert - проверяем редирект
        await _page.WaitForURLAsync("**/login");
        var title = await _page.TitleAsync();
        title.Should().Contain("Login");

        // Логинимся
        await _page.FillAsync("input[name='Email']", "user@example.com");
        await _page.FillAsync("input[name='Password']", "Str0ng!Pass");
        await _page.ClickAsync("button[type='submit']");
        await _page.WaitForURLAsync("**/dashboard");

        var dashboardText = await _page.TextContentAsync("h1");
        dashboardText.Should().Be("Dashboard");
    }

    public async Task DisposeAsync()
    {
        await _browser.CloseAsync();
        _playwright.Dispose();
    }
}

                
                

Блоги

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