Среди множество псевдоидей, витающих вокруг ТДД, есть некоторое пренебрежение к test doubles, не в последнюю очередь связанное с их смешными названиями.
Назвали бы их как-нибудь гордо, а то мок, стаб, фейк — ощущение, что мы ничего особо не теряем, если этим не пользуемся. (В противоположность «интеграционным тестам» и «реальным зависимостям»).
Однако можно поменять точку зрения. В конце концов, мок не только и не столько подпирает зависимый компонент, сколько специфицирует поведение зависимости. А «реальная» имплементация — это некое текущее, возможно ошибочное воплощение нашей гордой идеи.
В этом смысле каждый раз, когда мы пишем
мы документируем некоторое поведение компонента.
Отсюда — несколько следствий.
Мок-объекты плотно связаны с собственно спецификациями тестов, и по вокабуляру, и по смыслу. И то, и другое фокусирует нас на требованиях и сценариях, а не на особенностях текущей имплементации.
Нам немного мешает тут видеть спецификацию тот факт, что утверждения о мокируемом компоненте в случае использования фреймворков типа Mockito — распылены по классам-клиентам, и за ними трудно уследить. В этом смысле явные классы-наследники с предопределенным поведением предпочтительнее моков типа Mockito, потому что позволяют инкапсулировать сценарии и обозревать их одним взглядом.
Таким образом, к каждому классу OrderProvider добавляются классы-сценарии ValidOrderProviderStub, ExpiredOrderProviderStub, InvalidOrderIdException_OrderProviderStub, OnlyOnceOrderProvider и так далее, которые могут находиться в одном пакете и использоваться всеми тестами сразу.
Это включает в себя Spy-классы, которые нетрудно имплементировать самому.
Кроме того, такой подход быстрее выполняется и работает без использования рефлексии.
Таким образом, при чтении кода мы можем сделать конвенцию, что наличие мока подтверждает возможность сценария, а его отсутствие запрещает наличие такого сценария. Если мы запретим клиентским классам писать mockito expectations, то в поисках NullOrderProvider выяснится, что подходящего мока не имеется, потому что null никогда не возвращается, и поэтому тестировать на этот сценарий нет смысла.
Если мок — это спецификация, то мок — отражение наших планов на компонент, и они могут быть и должны быть написаны вперед реализации.
Если мок — это спецификация, то она устроена иерархично и отражает поведение компонента в зависимости от определенных условий.
Таким образом, наши моки могут размножиться слишком сильно: безусловные моки, всегда возвращающие то же самое, а также моки с внутренними условиями, типа forIdOne_returningOne_forIdTwo_ReturningTwo_OrderProvider. Последнее перебор и следует просто создать FakeOrderProvider с соответствующим поведением, которое следует синхронизировать с реальной имплементацией.
Соответственно, если мок — это спецификация определенного поведения, то имплементация компонента — не что иное, как синхронизация поведения компонента с его моками.
И это, пожалуй, главный аргумент против моков — необходимость синхронизировать их поведение с текущей реальной имплементацией, об этом поговорим отдельно в следующий раз.
Назвали бы их как-нибудь гордо, а то мок, стаб, фейк — ощущение, что мы ничего особо не теряем, если этим не пользуемся. (В противоположность «интеграционным тестам» и «реальным зависимостям»).
Однако можно поменять точку зрения. В конце концов, мок не только и не столько подпирает зависимый компонент, сколько специфицирует поведение зависимости. А «реальная» имплементация — это некое текущее, возможно ошибочное воплощение нашей гордой идеи.
В этом смысле каждый раз, когда мы пишем
when(mockDependency.method(inputValue)).thenReturn(returnValue)
, мы документируем некоторое поведение компонента.
Отсюда — несколько следствий.
Мок-объекты плотно связаны с собственно спецификациями тестов, и по вокабуляру, и по смыслу. И то, и другое фокусирует нас на требованиях и сценариях, а не на особенностях текущей имплементации.
Нам немного мешает тут видеть спецификацию тот факт, что утверждения о мокируемом компоненте в случае использования фреймворков типа Mockito — распылены по классам-клиентам, и за ними трудно уследить. В этом смысле явные классы-наследники с предопределенным поведением предпочтительнее моков типа Mockito, потому что позволяют инкапсулировать сценарии и обозревать их одним взглядом.
Таким образом, к каждому классу OrderProvider добавляются классы-сценарии ValidOrderProviderStub, ExpiredOrderProviderStub, InvalidOrderIdException_OrderProviderStub, OnlyOnceOrderProvider и так далее, которые могут находиться в одном пакете и использоваться всеми тестами сразу.
Это включает в себя Spy-классы, которые нетрудно имплементировать самому.
Кроме того, такой подход быстрее выполняется и работает без использования рефлексии.
Таким образом, при чтении кода мы можем сделать конвенцию, что наличие мока подтверждает возможность сценария, а его отсутствие запрещает наличие такого сценария. Если мы запретим клиентским классам писать mockito expectations, то в поисках NullOrderProvider выяснится, что подходящего мока не имеется, потому что null никогда не возвращается, и поэтому тестировать на этот сценарий нет смысла.
Если мок — это спецификация, то мок — отражение наших планов на компонент, и они могут быть и должны быть написаны вперед реализации.
Если мок — это спецификация, то она устроена иерархично и отражает поведение компонента в зависимости от определенных условий.
Таким образом, наши моки могут размножиться слишком сильно: безусловные моки, всегда возвращающие то же самое, а также моки с внутренними условиями, типа forIdOne_returningOne_forIdTwo_ReturningTwo_OrderProvider. Последнее перебор и следует просто создать FakeOrderProvider с соответствующим поведением, которое следует синхронизировать с реальной имплементацией.
Соответственно, если мок — это спецификация определенного поведения, то имплементация компонента — не что иное, как синхронизация поведения компонента с его моками.
И это, пожалуй, главный аргумент против моков — необходимость синхронизировать их поведение с текущей реальной имплементацией, об этом поговорим отдельно в следующий раз.