Mock-объекты и фикстуры в C#: секрет идеального юнит-теста

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

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

Начать курс

Почему ваши тесты лгут, и как это исправить?

Представьте: вы пишете идеальный код на 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 сама генерирует уникальные значения для строк, чисел, дат и даже для вложенных списков. Вы можете кастомизировать генерацию

Блоги

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