В чем разница между mock и stub в тестировании?

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

Ответ

Mock и Stub — это виды тестовых заглушек (test doubles), используемых для изоляции тестируемого кода от его зависимостей. Основное различие — в их цели и способе взаимодействия с тестом.

Stub (Заглушка)

  • Цель: Предоставить тестируемому коду контролируемые входные данные, чтобы протестировать его поведение при разных условиях. Stub заменяет реальную зависимость, возвращая заранее заданные, «жестко закодированные» ответы.
  • Взаимодействие: Тест не проверяет, как тестируемый код взаимодействовал с заглушкой. Важен только конечный результат работы тестируемого кода.
  • Аналогия: Короткий опросник с заранее известными ответами (A, B, C). Вы проверяете, как система обрабатывает эти ответы, но не следите за самим процессом заполнения.

Пример Stub (используя Moq):

// Интерфейс зависимости
public interface IDiscountService
{
    decimal GetDiscount(string customerCode);
}

// Тест с использованием Stub
[Test]
public void CalculateTotal_WithPremiumCustomer_AppliesDiscount()
{
    // 1. ARRANGE: Создаем Stub
    var discountStub = new Mock<IDiscountService>();
    // Настраиваем Stub всегда возвращать скидку 10% для любого кода
    discountStub.Setup(s => s.GetDiscount(It.IsAny<string>())).Returns(0.10m);

    var calculator = new OrderCalculator(discountStub.Object);
    var order = new Order { Total = 100.00m, CustomerCode = "PREMIUM123" };

    // 2. ACT: Вызываем тестируемый метод
    var finalTotal = calculator.CalculateTotal(order);

    // 3. ASSERT: Проверяем РЕЗУЛЬТАТ работы тестируемого кода
    Assert.AreEqual(90.00m, finalTotal); // Ожидаем 100 - 10%
    // НЕТ проверок на то, как вызывался discountStub
}

Mock (Макет)

  • Цель: Проверить взаимодействие (behavior) тестируемого кода с зависимостью. Mock заменяет зависимость и записывает, какие методы были вызваны, с какими параметрами и сколько раз.
  • Взаимодействие: Тест явно проверяет (verifies), что ожидаемые вызовы к mock-объекту действительно произошли. Это проверка процесса, а не только результата.
  • Аналогия: Регистратор нажатий клавиш. Вы проверяете, что пользователь нажал именно клавиши 'Ctrl+S', а не просто смотрите на сохраненный файл.

Пример Mock (используя Moq):

public interface IOrderRepository
{
    void Save(Order order);
}

[Test]
public void ProcessOrder_ValidOrder_SavesToRepository()
{
    // 1. ARRANGE: Создаем Mock
    var repositoryMock = new Mock<IOrderRepository>();
    // Настраиваем mock, но возвращать нечего (void метод)

    var processor = new OrderProcessor(repositoryMock.Object);
    var order = new Order { Id = 1, Total = 50.00m };

    // 2. ACT
    processor.ProcessOrder(order);

    // 3. ASSERT: Проверяем ВЗАИМОДЕЙСТВИЕ (был ли вызван Save с правильным order)
    repositoryMock.Verify(r => r.Save(order), Times.Once); // КРИТИЧНАЯ ПРОВЕРКА MOCK
    // Также можно проверить результат, если он есть
}

Итог:

  • Используйте Stub, когда вам нужно задать состояние (подменить данные) для теста.
  • Используйте Mock, когда вам нужно проверить поведение (удостовериться, что определенный метод был вызван).

На практике библиотеки вроде Moq или NSubstitute позволяют создавать объекты, которые могут быть и stub, и mock одновременно. Различие становится понятием в голове тестировщика: «Я использую этот объект как stub, чтобы подставить данные» или «Я использую его как mock и буду проверять вызовы».