Почему ваши тесты лгут, и как это исправить?
Представьте: вы пишете идеальный код на C#, покрываете его тестами, зелёная галочка горит, CI-пайплайн счастлив. Вы выкатываете релиз… и прод падает. Знакомо? Скорее всего, проблема не в коде, а в тестах. Они тестировали не ваш настоящий код, а его «игрушечную» версию, привязанную к базе данных, файловой системе или внешнему API.
Добро пожаловать в мир mock-объектов и фикстур — двух столпов, на которых держится качественное юнит-тестирование в C#. Без них ваши тесты — это хрупкие интеграционные проверки, которые ломаются от малейшего изменения окружения. С ними — вы получаете быстрые, изолированные и надёжные тесты, которые реально защищают код.
В этой статье мы разберём: чем mock отличается от stub и fake, как правильно настраивать фикстуры, и покажем живые примеры с популярными библиотеками: Moq, NSubstitute и AutoFixture. Поехали!
Mock, Stub, Fake — в чём разница? (Спойлер: это важно)
Прежде чем писать код, давайте договоримся о терминах. Многие разработчики используют слово «mock» для всего подряд, но это ведёт к путанице. В классификации Gerard Meszaros (книга «xUnit Test Patterns») есть чёткое разделение:
- Dummy — объект, который передаётся, но никогда не используется. Нужен только для заполнения параметров.
- Stub — предоставляет заранее подготовленные ответы на вызовы. Вы проверяете состояние (state verification).
- Fake — рабочая, но упрощённая реализация (например, InMemoryRepository вместо реальной БД).
- Mock — объект, который проверяет поведение (behavior verification). Вы утверждаете, что определённый метод был вызван с определёнными аргументами.
На практике в C# мы чаще всего используем mock-объекты (через Moq или NSubstitute) для проверки взаимодействий, и фикстуры (через AutoFixture) для быстрой генерации тестовых данных. Но давайте по порядку.
Mock-объекты в C#: изолируем зависимости
Представьте, что у вас есть сервис OrderService, который при оформлении заказа отправляет email и сохраняет данные в БД. Тестировать его «по-настоящему» — значит каждый раз слать письма и писать в базу. Это медленно, ненадёжно и дорого.
Решение — изоляция зависимостей. Мы заменяем реальные объекты (EmailSender, DbContext) на mock-объекты и проверяем, что сервис вызывает нужные методы с правильными параметрами.
Пример с Moq (самая популярная библиотека)
public interface IEmailSender
{
void Send(string to, string subject, string body);
}
public interface IOrderRepository
{
void Save(Order order);
}
public class OrderService
{
private readonly IEmailSender _emailSender;
private readonly IOrderRepository _repository;
public OrderService(IEmailSender emailSender, IOrderRepository repository)
{
_emailSender = emailSender;
_repository = repository;
}
public void PlaceOrder(Order order)
{
// Бизнес-логика
_repository.Save(order);
_emailSender.Send(order.CustomerEmail, "Заказ подтверждён", $"Ваш заказ #{order.Id} оформлен.");
}
}
Теперь напишем тест с использованием Moq:
[TestFixture]
public class OrderServiceTests
{
[Test]
public void PlaceOrder_ShouldSendEmail_WhenOrderIsValid()
{
// Arrange
var mockEmailSender = new Mock<IEmailSender>();
var mockRepository = new Mock<IOrderRepository>();
var service = new OrderService(mockEmailSender.Object, mockRepository.Object);
var order = new Order { Id = 1, CustomerEmail = "test@example.com" };
// Act
service.PlaceOrder(order);
// Assert — проверяем поведение
mockEmailSender.Verify(
s => s.Send("test@example.com", "Заказ подтверждён", It.IsAny<string>()),
Times.Once);
mockRepository.Verify(
r => r.Save(order),
Times.Once);
}
}
Обратите внимание: мы не проверяем, что письмо реально ушло. Мы проверяем, что метод Send был вызван ровно один раз с правильным email. Это и есть behavior verification — суть mock-объектов.
NSubstitute: более лаконичный синтаксис
Если Moq кажется вам многословным, попробуйте NSubstitute. Тот же тест выглядит так:
[Test]
public void PlaceOrder_ShouldSendEmail_NSubstitute()
{
var emailSender = Substitute.For<IEmailSender>();
var repository = Substitute.For<IOrderRepository>();
var service = new OrderService(emailSender, repository);
var order = new Order { Id = 1, CustomerEmail = "test@example.com" };
service.PlaceOrder(order);
// Проверка вызова
emailSender.Received(1).Send("test@example.com", "Заказ подтверждён", Arg.Any<string>());
repository.Received(1).Save(order);
}
Код стал короче, читаемость выше. Нет .Object, нет лишних обёрток — чистая магия расширений.
Фикстуры: готовим тестовые данные без боли
Теперь представьте, что ваш Order — это не простой класс, а монстр с 15 полями, вложенными объектами и коллекциями. Создавать его вручную в каждом тесте — ад. Тут на помощь приходят фикстуры.
Фикстура — это набор предварительно настроенных данных или объектов, которые используются в тестах. В C# есть замечательная библиотека AutoFixture, которая умеет автоматически создавать объекты любого типа.
Пример с AutoFixture
public class Order
{
public int Id { get; set; }
public string CustomerEmail { get; set; }
public DateTime CreatedAt { get; set; }
public List<OrderItem> Items { get; set; }
public decimal TotalAmount { get; set; }
}
[Test]
public void PlaceOrder_WithAutoFixture_ShouldWork()
{
// Arrange
var fixture = new Fixture();
var order = fixture.Create<Order>(); // Автоматически заполняет все поля!
var mockEmailSender = new Mock<IEmailSender>();
var mockRepository = new Mock<IOrderRepository>();
var service = new OrderService(mockEmailSender.Object, mockRepository.Object);
// Act
service.PlaceOrder(order);
// Assert
mockEmailSender.Verify(s => s.Send(order.CustomerEmail, It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
AutoFixture сама генерирует уникальные значения для строк, чисел, дат и даже для вложенных списков. Вы можете кастомизировать генерацию