Чтобы не заставлять вас бегать к предыдущей статье, приведу правило «Не тестируем алгоритм методов» еще раз, как оно там звучало:
Тут неудачно подобрано название правила, но лучшего пока не придумал. Среди «мокистов» (это те, кто мокает в тестах) есть те, кто проверяет количество вызовов определенных методов, верифицирует сам вызов и пр. Другими словами, занимается проверкой внутренней работы методов. Это так же плохо, как и тестирование приватных. Разница только в уровне применения такой проверки. Такой подход опять дает множество хрупких тестов, из-за чего 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<Event>(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<IEventValidator>(); var dbCommands = new Mock<IDatabaseCommands>(); var messagingLogger = new Mock<IMessagingLogger>(); 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<Event>(), Times.Once()) messagingLogger.Verify(m => m.Received(It.IsAny<EventPayload>(), Times.Once()) }
Подход «немокиста»
Он создал бы fake objects и проверил бы совершилось ли событие в целом, а не вызов метода. В этом случае код был бы примерно следующим:
public void Should_save_valid_data_and_log_to_messaging_events() { var builder = new EventPayload { // skipped }; var validator = Mock.Of<IEventValidator>(); 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.

