Что такое Moq и для чего он используется?

«Что такое Moq и для чего он используется?» — вопрос из категории Тестирование, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Moq (произносится "Mock-you") — это библиотека для .NET, предназначенная для создания мок-объектов (test doubles) в модульных тестах. Её основная цель — изолировать тестируемый класс, подменяя его реальные зависимости контролируемыми "заглушками" с предопределённым поведением.

Зачем это нужно? Представьте, вы тестируете OrderService, который зависит от IEmailService. Вместо того чтобы в тестах реально отправлять письма (это медленно и имеет побочные эффекты), вы создаёте мок IEmailService, который лишь имитирует отправку, позволяя проверить, что OrderService корректно его вызывает.

Ключевые концепции Moq:

  1. Mock<T>: Основной класс для создания мока интерфейса T или класса с виртуальными членами.
  2. Setup(): Настройка поведения мока. Вы указываете, какой метод/свойство вызывается и что должно произойти.
  3. Returns(): Определяет значение, которое должен вернуть настроенный метод.
  4. Verify(): Проверка (assert), что определённый метод был вызван с ожидаемыми параметрами нужное количество раз.

Практический пример:

// Интерфейс зависимости
public interface IOrderRepository
{
    Order GetOrder(int id);
    void Save(Order order);
}

// Класс, который мы тестируем
public class OrderProcessor
{
    private readonly IOrderRepository _repository;
    public OrderProcessor(IOrderRepository repository) => _repository = repository;

    public bool ProcessOrder(int orderId)
    {
        var order = _repository.GetOrder(orderId);
        if (order == null) return false;

        order.Status = "Processed";
        _repository.Save(order);
        return true;
    }
}

// ТЕСТ с использованием Moq и xUnit
[Fact]
public void ProcessOrder_ValidOrder_UpdatesStatusAndSaves()
{
    // 1. ARRANGE: Подготовка данных и создание мока
    var testOrder = new Order { Id = 123, Status = "New" };
    var mockRepo = new Mock<IOrderRepository>();

    // Настраиваем мок: при вызове GetOrder с аргументом 123 вернуть testOrder
    mockRepo.Setup(repo => repo.GetOrder(123)).Returns(testOrder);
    // Настройка для Save не требуется, если нам важен только факт вызова

    var processor = new OrderProcessor(mockRepo.Object);

    // 2. ACT: Выполнение тестируемого метода
    bool result = processor.ProcessOrder(123);

    // 3. ASSERT: Проверки
    Assert.True(result); // Метод вернул true
    Assert.Equal("Processed", testOrder.Status); // Статус изменился
    // Верификация: убеждаемся, что Save был вызван ровно 1 раз с нашим order
    mockRepo.Verify(repo => repo.Save(testOrder), Times.Once);
}

Важные особенности и лучшие практики:

  • Мокирование классов: Moq может мокировать только виртуальные методы, свойства и абстрактные члены классов. Для моков предпочтительнее использовать интерфейсы.
  • Строгие (Strict) vs Нестрогие (Loose) моки: По умолчанию Moq создаёт нестрогие моки. Вызов ненастроенного метода вернёт значение по умолчанию. MockBehavior.Strict выбросит исключение при любом неожиданном вызове, что делает тесты более хрупкими.
  • Проверка аргументов: Verify и Setup поддерживают гибкую проверку аргументов через It.IsAny<T>(), It.Is<T>(predicate).
    mockRepo.Verify(repo => repo.Save(It.Is<Order>(o => o.Status == "Processed")), Times.Once);
  • Не злоупотребляйте моками: Избыточное мокирование (over-mocking) усложняет тесты. Мокайте только внешние зависимости (база данных, файловая система, API), а не простые объекты-значения или статические утилиты.