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

Ответ

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), а не простые объекты-значения или статические утилиты.

Ответ 18+ 🔞

Слушай, вот есть такая штука — Moq (читается "мок-ю", типа "мокует тебя"). Это библиотека для .NET, чтобы в тестах создавать эти... как их... подставные объекты, ну, заглушки. Суть в чём: ты тестируешь один класс, а он там с кем-то общается — с базой данных, с почтовым сервисом, с какой-нибудь другой хренью. Так вот, вместо того чтобы в тестах реально лезть в базу (это медленно, нестабильно и вообще пиздец), ты подсовываешь ему подделку, которую сам и настраиваешь. И всё изолированно, красиво.

Ну зачем это, блядь, надо? Ну смотри. Допустим, у тебя есть OrderService, и он шлёт письма через IEmailService. В тесте тебе не нужно, чтобы письма реально улетали — тебе нужно просто проверить, что твой сервис пытается его вызвать с правильными параметрами. Вот для этого мок и нужен.

Основные штуки в 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; // Если заказа нет — false

        order.Status = "Processed"; // Меняем статус
        _repository.Save(order); // Сохраняем
        return true;
    }
}

// А вот сам тест (допустим, 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 был вызван ровно ОДИН раз с нашим order
    mockRepo.Verify(repo => repo.Save(testOrder), Times.Once);
}

Важные моменты, чтобы не наступить в говно:

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