Как протестировать интерфейс Consumer с помощью Unit Testing?

Ответ

Для тестирования зависимостей, реализующих интерфейс IConsumer<T>, используйте библиотеки для мокинга (Moq, NSubstitute) и проверяйте взаимодействие с ними в соответствии с контрактом.

Пример теста с Moq:

// Тестируемый сервис, зависящий от IConsumer<Order>
public class OrderProcessor
{
    private readonly IConsumer<Order> _consumer;

    public OrderProcessor(IConsumer<Order> consumer) 
        => _consumer = consumer;

    public void Process(Order order) 
        => _consumer.Consume(order);
}

// Тест
[Test]
public void Process_ShouldCallConsumerExactlyOnce_WithCorrectOrder()
{
    // Arrange
    var mockConsumer = new Mock<IConsumer<Order>>();
    var processor = new OrderProcessor(mockConsumer.Object);
    var testOrder = new Order { Id = 123 };

    // Act
    processor.Process(testOrder);

    // Assert
    mockConsumer.Verify(
        c => c.Consume(It.Is<Order>(o => o.Id == testOrder.Id)), 
        Times.Once
    );
}

Ключевые аспекты тестирования Consumer:

  1. Изоляция: Mock заменяет реальную реализацию IConsumer<T>, позволяя тестировать только поведение класса, который его использует.
  2. Проверка вызовов: Убедитесь, что метод Consume вызывается ожидаемое количество раз (Times.Once, Times.Never).
  3. Проверка аргументов: Используйте It.Is<T>() для точной проверки переданных данных.
  4. Тестирование исключений: Можно настроить mock на выброс исключения и проверить реакцию основного кода.
// Пример теста на обработку исключения от Consumer
[Test]
public void Process_ShouldLogError_WhenConsumerThrows()
{
    var mockConsumer = new Mock<IConsumer<Order>>();
    mockConsumer.Setup(c => c.Consume(It.IsAny<Order>()))
                .Throws(new InvalidOperationException("Queue is full"));
    var mockLogger = new Mock<ILogger<OrderProcessor>>();
    var processor = new OrderProcessor(mockConsumer.Object, mockLogger.Object);

    Assert.Throws<InvalidOperationException>(() => processor.Process(new Order()));
    mockLogger.Verify(l => l.LogError(It.IsAny<string>()), Times.Once);
}

Ответ 18+ 🔞

А, ну вот, опять эти ваши интерфейсы, моки и прочая хрень. Слушай, а давай я тебе на пальцах объясню, как это всё работает, а то ты, я смотрю, в документации утонул.

Представь себе, есть у тебя какой-нибудь OrderProcessor — это такой парень, который не сам заказы обрабатывает, а скидывает их какому-то другому чуваку, типа курьеру. А курьер у нас — это IConsumer<Order>. И вот задача: проверить, что наш парень действительно скинул заказ, и скинул правильный, и не два раза, а один.

Так вот, чтобы не ждать настоящего курьера, который, не дай бог, застрянет в лифте или ещё чего, мы подсовываем муляж. В мире C# это называется Mock. Берём библиотеку типа Moq и делаем подставную фигню.

Смотри, как это выглядит в жизни, а не в сухих мануалах:

// Это наш работяга, который должен передать заказ
public class OrderProcessor
{
    private readonly IConsumer<Order> _consumer; // А это его связной

    public OrderProcessor(IConsumer<Order> consumer) 
        => _consumer = consumer; // Пристроили ему курьера

    public void Process(Order order) 
        => _consumer.Consume(order); // И он просто тыкает заказ в метод Consume
}

// А теперь тест, где мы всё проверяем
[Test]
public void Process_ShouldCallConsumerExactlyOnce_WithCorrectOrder()
{
    // Arrange — готовим сцену для спектакля
    var mockConsumer = new Mock<IConsumer<Order>>(); // Вот наш муляж курьера
    var processor = new OrderProcessor(mockConsumer.Object); // Даём работяге муляж
    var testOrder = new Order { Id = 123 }; // А вот и тестовый заказ

    // Act — даём команду "пли!"
    processor.Process(testOrder);

    // Assert — а теперь проверяем, не накосячил ли кто
    mockConsumer.Verify(
        c => c.Consume(It.Is<Order>(o => o.Id == testOrder.Id)), // Проверяем, что в Consume сунули именно заказ с Id = 123
        Times.Once // И сунули ровно ОДИН раз, а не десять, как последний раз у Васи
    );
}

Вот и вся магия. Мы не тестируем, что делает настоящий Consumer (он там может письмо с заказом на Луну отправлять). Мы тестируем, что наш OrderProcessor правильно взаимодействует с тем, кому он должен передать данные. Это и есть изоляция — отрезали всё лишнее, смотрим на конкретное поведение.

На что ещё смотреть надо, чтобы начальство не ебло:

  1. Сколько раз дернули метод. Times.Once, Times.Never, Times.Exactly(5) — это важно. А то вдруг твой метод в цикле запустился и спамит вызовами.
  2. Что именно передали. It.Is<Order>(o => o.Id == testOrder.Id) — это фильтр. Можно проверить что угодно: айдишник, имя, сумму. Главное, чтобы заказ не подменили по дороге.
  3. А если курьер обосрался? Надо проверить, как твой код реагирует на исключения. Настраиваешь мок на выброс ошибки и смотришь, не падает ли всё к чертям, и логируется ли это.

Вот, например, как проверить, что при ошибке хотя бы в лог напишут:

[Test]
public void Process_ShouldLogError_WhenConsumerThrows()
{
    // Подсовываем кривого курьера, который всегда падает с ошибкой
    var mockConsumer = new Mock<IConsumer<Order>>();
    mockConsumer.Setup(c => c.Consume(It.IsAny<Order>()))
                .Throws(new InvalidOperationException("Queue is full, блядь!")); // Имитируем реальную проблему
    var mockLogger = new Mock<ILogger<OrderProcessor>>(); // Мок для логгера тоже
    var processor = new OrderProcessor(mockConsumer.Object, mockLogger.Object);

    // Ожидаем, что при обработке вылетит исключение
    Assert.Throws<InvalidOperationException>(() => processor.Process(new Order()));

    // И заодно проверяем, что в лог об этой жопе хоть что-то записали
    mockLogger.Verify(l => l.LogError(It.IsAny<string>()), Times.Once);
}

Короче, суть в чём: моки — это твои подставные актёры. Ты говоришь им, как себя вести ("когда вызовут Consume, кинь вот это исключение"), а потом проверяешь, что основной актёр (OrderProcessor) отыграл свою роль правильно и не забыл реплику. Всё просто, как три копейки. Главное — не перемудрить и проверять именно то, что нужно по контракту, а не всю вселенную.