Урок по треку C# и .NET 6 мин

Тестирование в .NET

xUnit, моки, интеграционные тесты с WebApplicationFactory, тестовые базы и пирамида.

От .NET-разработчика ждут уверенного xUnit, понимания моков и умения писать интеграционные тесты на ASP.NET Core с настоящей базой.

xUnit

public class CalculatorTests
{
    [Fact]
    public void Divide_by_zero_throws() =>
        Assert.Throws<DivideByZeroException>(() => Calculator.Divide(1, 0));

    [Theory]
    [InlineData(2, 3, 5)]
    [InlineData(-1, 1, 0)]
    public void Adds(int a, int b, int expected) =>
        Assert.Equal(expected, Calculator.Add(a, b));
}
  • Новый экземпляр класса на каждый тест: конструктор вместо setup, IDisposable/IAsyncLifetime вместо teardown.
  • Общая дорогая инициализация: IClassFixture<T>, ICollectionFixture<T>.
  • Асинхронный тест возвращает Task, никогда не async void.

NUnit и MSTest тоже встречаются, принципы те же.

Структура теста

Arrange, Act, Assert. Один тест проверяет одно поведение, имя описывает сценарий: Withdraw_more_than_balance_returns_error. Для читаемых проверок многие используют FluentAssertions или Shouldly.

Моки

Стаб отдаёт заготовленные ответы, мок ещё и проверяет взаимодействие.

var mail = new Mock<IMailSender>();
var sut = new RegistrationService(repo, mail.Object);

await sut.RegisterAsync("ira@example.com");

mail.Verify(m => m.SendAsync("ira@example.com", It.IsAny<string>()), Times.Once);

Мокайте границы системы (почта, платежи, внешние API), а не всё подряд. Тест, который проверяет каждый внутренний вызов, ломается от любого рефакторинга. Не мокайте DbContext и IQueryable: мок не воспроизводит перевод в SQL.

Время и случайность

Код с DateTime.UtcNow нетестируем. В .NET 8 внедряйте TimeProvider, в тестах FakeTimeProvider с Advance(...).

Интеграционные тесты ASP.NET Core

public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public OrdersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(b => b.ConfigureTestServices(s =>
            s.AddSingleton<IPaymentGateway, FakePaymentGateway>())).CreateClient();
    }

    [Fact]
    public async Task Get_unknown_order_returns_404()
    {
        var resp = await _client.GetAsync("/orders/999999");
        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
    }
}

WebApplicationFactory поднимает настоящий конвейер в памяти: DI, middleware, сериализация, валидация. Чтобы Program был виден из тестов, добавляют public partial class Program;.

База в тестах

InMemory-провайдер EF Core не проверяет ограничения и транзакции, поэтому для логики работы с данными берут настоящую СУБД: Testcontainers поднимает PostgreSQL в Docker из кода теста. Между тестами данные чистят (Respawn) или откатывают транзакцию. SQLite in-memory это компромисс, если Docker недоступен.

Пирамида

Много быстрых модульных тестов, меньше интеграционных, немного сквозных. Для бэкенда часто выгоден «трофей»: упор на интеграционные тесты API, которые дают больше уверенности за те же усилия.

Частые ошибки

  • async void в тестах.
  • Зависимость тестов друг от друга через общие данные.
  • Проверка реализации вместо поведения.
  • InMemory вместо настоящей базы.
  • Thread.Sleep в тестах вместо управляемого времени.

Что спросят на собесе

  • Чем [Fact] отличается от [Theory]?
  • Чем мок отличается от стаба, когда моки вредят?
  • Как написать интеграционный тест для ASP.NET Core?
  • Почему не стоит тестировать на InMemory-провайдере?
  • Как тестировать код, зависящий от текущего времени?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.