Тестовые двойники: Moq и NSubstitute
↑ 2.4 Тестирование в .NET — основы · ← Предыдущая · → Следующая
Зачем это на собесе
Ждут, что вы отличаете 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.
Практика
- Замените мок репозитория на in-memory fake и сравните хрупкость тестов.
- Проверьте отправку письма моком, а расчёт цены — без моков.
- Напишите тест на повторную попытку с помощью stub, который сначала бросает исключение.
Вопросы с ответами
Чем mock отличается от stub?
Stub подаёт данные в тест, mock проверяет, что вызов произошёл. Проверять нужно результат; моки — для побочных эффектов на границах.
Когда мокать не стоит?
Для собственной чистой логики, DbContext и типов сторонних библиотек: тест перестаёт отражать реальную работу.
Moq или NSubstitute?
Функционально близки: у NSubstitute проще синтаксис, у Moq больше распространён. Важнее договориться о единстве стиля в команде.