Заменяем тестирование алгоритмов тестированием вносимых эффектов
Как и ожидал, правило 8 о том, что не тестируем алгоритм методов в статье «Правила внедрения TDD в старом проекте» вызвало больше всего вопросов «как» и «зачем». В момент составления прошлой статьи мне показалось это очевидным, поэтому не остановился детальнее на этом моменте. Но т.к. вопросов возникло много, хочу описать своё видение. Поэтому под катом будет небольшой пример кода и два примера того, как его можно было бы протестировать.
Чтобы не заставлять вас бегать к предыдущей статье, приведу правило «Не тестируем алгоритм методов» еще раз, как оно там звучало:
Есть следующий код хендлера:
Необходимо протестировать работу метода Handle (). Вопрос стоит в том, чтобы убедиться, что методы DbCommands и MessagingLogger были вызваны.
Он передал бы в конструктор класса моки соответствующих интерфейсов, а после проверил бы вызвались или нет соответствующие методы: SaveEvt (), Received () или InvalidEvent (). Код выглядел бы примерно так:
Он создал бы fake objects и проверил бы совершилось ли событие в целом, а не вызов метода. В этом случае код был бы примерно следующим:
А методы fake-objects выглядели бы так:
При этом IsEventSaved был бы объявлен только в fake объекте.
Первый подход прост и быстр, но если необходимо менять методы, вызывать один, вместо другого в той же ситуации, то тесты необходимо было бы править.
Чтобы не заставлять вас бегать к предыдущей статье, приведу правило «Не тестируем алгоритм методов» еще раз, как оно там звучало:
Тут неудачно подобрано название правила, но лучшего пока не придумал. Среди «мокистов» (это те, кто мокает в тестах) есть те, кто проверяет количество вызовов определенных методов, верифицирует сам вызов и пр. Другими словами, занимается проверкой внутренней работы методов. Это так же плохо, как и тестирование приватных. Разница только в уровне применения такой проверки. Такой подход опять дает множество хрупких тестов, из-за чего TDD некоторыми не воспринимается нормально.
Есть следующий код хендлера:
public class SomeEventHandler
{
public SomeEventHandler(IDatabaseCommands dbCommands,
IEventValidator validator,
IMessagingLogger messagingLogger)
{
// skipped
}
public HandlerResult Handle(EventPayload payload)
{
if (Validator.IsOurEvent(payload))
if (Validator.IsValid(payload))
{
var evt = Mapper.Map(payload);
try
{
using (var tran = new TransactionScope())
{
DbCommands.SaveEvt(evt);
MessagingLogger.Received(payload);
tran.Complete();
}
}
catch (Exception ex)
{
return MessageHandlerResult.Fatal;
}
}
else
{
var error = Validator.GetErrors();
MessagingLogger.InvalidEvent(payload, error);
return MessageHandlerResult.Fatal;
}
return MessageHandlerResult.Success;
}
}
Необходимо протестировать работу метода Handle (). Вопрос стоит в том, чтобы убедиться, что методы DbCommands и MessagingLogger были вызваны.
Подход «мокиста»
Он передал бы в конструктор класса моки соответствующих интерфейсов, а после проверил бы вызвались или нет соответствующие методы: SaveEvt (), Received () или InvalidEvent (). Код выглядел бы примерно так:
public void Should_save_valid_data_and_log_to_messaging_events()
{
var builder = new EventPayload {
// skipped
};
var validator = Mock.Of();
var dbCommands = new Mock();
var messagingLogger = new Mock();
var handler = new SomeEventHandler(dbCommands, validator, messagingLogger);
var result = handler.Handle(payload);
// assertions
Assert.Equal(MessageHandlerResult.Success, result);
dbCommands.Verify(m => m.SaveEvt(It.IsAny(), Times.Once())
messagingLogger.Verify(m => m.Received(It.IsAny(), Times.Once())
}
Подход «немокиста»
Он создал бы fake objects и проверил бы совершилось ли событие в целом, а не вызов метода. В этом случае код был бы примерно следующим:
public void Should_save_valid_data_and_log_to_messaging_events()
{
var builder = new EventPayload {
// skipped
};
var validator = Mock.Of();
var dbCommands = new FakeDatabaseCommands();
var messagingLogger = new FakeMessagingLogger();
var handler = new SomeEventHandler(dbCommands, validator, messagingLogger);
var result = handler.Handle(payload);
// assertions
Assert.Equal(MessageHandlerResult.Success, result);
Assert.True(dbCommands.IsEventSaved);
Assert.True(messagingLogger.IsEventRegistered);
}
А методы fake-objects выглядели бы так:
public void SaveEvt(Event evt)
{
IsEventSaved = true;
}
При этом IsEventSaved был бы объявлен только в fake объекте.
Плюсы и Минусы
Первый подход прост и быстр, но если необходимо менять методы, вызывать один, вместо другого в той же ситуации, то тесты необходимо было бы править.
Второй подход приводит к созданию дополнительных сущностей, а выигрыш получается только в ситуации с заменой методов. В этом случае, возможно, даже ничего не придется менять ни в Fakes, ни в тестах. Еще один плюс, правда, более идеалистический, в том, что немокист делает тест так, чтобы он не знал о внутреннем устройстве тестируемого метода. Поэтому, лично я, если время позволяет, делаю тесты на fakes.