Тестовые двойники: Moq и NSubstitute

↑ 2.4 Тестирование в .NET — основы · ← Предыдущая · → Следующая

Обязательно~4 мин чтенияУровень: middle

Зачем это на собесе

Ждут, что вы отличаете mock, stub и fake и понимаете, где мок вредит. Частая ловушка — «мокать всё».

Объяснение

ДвойникЧто делает
Dummyзаглушка параметра, не используется
Stubвозвращает заранее заданные данные
Fakeупрощённая рабочая реализация (in-memory репозиторий)
Mockпроверяет, что взаимодействие произошло
Spyзаписывает вызовы для последующей проверки

Принцип: stub для входящих зависимостей, mock только для исходящих побочных эффектов (отправка письма, публикация события).

NSubstitute

var clock = Substitute.For<IClock>();
clock.UtcNow.Returns(new DateTime(2025, 1, 1));
 
var mailer = Substitute.For<IMailer>();
var sut = new OrderService(clock, mailer);
 
await sut.ConfirmAsync(order);
 
await mailer.Received(1).SendAsync(Arg.Is<Mail>(m => m.To == order.Email));

Moq

var repo = new Mock<IOrderRepository>();
repo.Setup(r => r.FindAsync(id, It.IsAny<CancellationToken>()))
    .ReturnsAsync(new Order(id));
 
var sut = new GetOrderHandler(repo.Object);
var result = await sut.Handle(new GetOrder(id), default);
 
repo.Verify(r => r.FindAsync(id, It.IsAny<CancellationToken>()), Times.Once);

Fake вместо мока

class InMemoryOrders : IOrderRepository
{
    private readonly Dictionary<Guid, Order> _db = new();
    public Task<Order?> FindAsync(Guid id, CancellationToken ct) => Task.FromResult(_db.GetValueOrDefault(id));
    public Task AddAsync(Order o, CancellationToken ct) { _db[o.Id] = o; return Task.CompletedTask; }
}

Fake устойчивее к рефакторингу, потому что проверяет результат, а не последовательность вызовов.

Нюансы и подводные камни

  • Не мокайте типы, которыми не владеете (DbContext, HttpClient): оборачивайте в свой интерфейс или используйте настоящие зависимости.
  • Верификация каждого вызова делает тест копией реализации.
  • Моки без Strict возвращают значения по умолчанию, ошибки настройки маскируются.
  • Moq использует динамические прокси и не умеет мокать не-виртуальные члены и static.
  • Для времени используйте TimeProvider и FakeTimeProvider.

Практика

  1. Замените мок репозитория на in-memory fake и сравните хрупкость тестов.
  2. Проверьте отправку письма моком, а расчёт цены — без моков.
  3. Напишите тест на повторную попытку с помощью stub, который сначала бросает исключение.

Вопросы с ответами

Связанные темы