Ответ
Для тестирования зависимостей, реализующих интерфейс 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:
- Изоляция: Mock заменяет реальную реализацию
IConsumer<T>, позволяя тестировать только поведение класса, который его использует. - Проверка вызовов: Убедитесь, что метод
Consumeвызывается ожидаемое количество раз (Times.Once,Times.Never). - Проверка аргументов: Используйте
It.Is<T>()для точной проверки переданных данных. - Тестирование исключений: Можно настроить 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 правильно взаимодействует с тем, кому он должен передать данные. Это и есть изоляция — отрезали всё лишнее, смотрим на конкретное поведение.
На что ещё смотреть надо, чтобы начальство не ебло:
- Сколько раз дернули метод.
Times.Once,Times.Never,Times.Exactly(5)— это важно. А то вдруг твой метод в цикле запустился и спамит вызовами. - Что именно передали.
It.Is<Order>(o => o.Id == testOrder.Id)— это фильтр. Можно проверить что угодно: айдишник, имя, сумму. Главное, чтобы заказ не подменили по дороге. - А если курьер обосрался? Надо проверить, как твой код реагирует на исключения. Настраиваешь мок на выброс ошибки и смотришь, не падает ли всё к чертям, и логируется ли это.
Вот, например, как проверить, что при ошибке хотя бы в лог напишут:
[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) отыграл свою роль правильно и не забыл реплику. Всё просто, как три копейки. Главное — не перемудрить и проверять именно то, что нужно по контракту, а не всю вселенную.