Не нарушают ли макеты принцип Open / Closed?

13

Некоторое время назад я прочитал в ответе о переполнении стека, которое не могу найти, предложение, объясняющее, что вы должны тестировать публичные API, и автор сказал, что вы должны тестировать интерфейсы. Автор также объяснил, что если реализация метода изменилась, вам не нужно изменять тестовый пример, поскольку это нарушит контракт, обеспечивающий работоспособность тестируемой системы. Другими словами, тест должен завершиться неудачей, если метод не работает, но не потому, что реализация изменилась.

Это привлекло мое внимание, когда мы говорим о насмешках. Поскольку моделирование в значительной степени зависит от ожидаемых вызовов от зависимостей тестируемой системы, моделирование тесно связано с реализацией, а не с интерфейсом.

При исследовании mock vs stub несколько статей соглашаются с тем, что вместо mock следует использовать заглушки, так как они не полагаются на ожидания от зависимостей, а это означает, что тест не требует знания базовой системы при реализации теста.

Мои вопросы будут:

  1. Нарушают ли насмешки принцип открытого / закрытого?
  2. Чего не хватает в аргументе в пользу заглушек в последнем абзаце, которые делают заглушки не такими уж хорошими по сравнению с насмешками?
  3. Если да, то когда будет хорошим вариантом использования для насмешки и когда будет хорошим вариантом использования заглушки?
Кристофер Франциско
источник
8
Since mocking relays heavily on expectation calls from system under test's dependencies...Я думаю, что это то место, где ты ошибаешься. Макет - это некое искусственное представление внешней системы. Он никоим образом не представляет внешнюю систему, за исключением того, что он моделирует внешнюю систему таким образом, что позволяет выполнять тесты с кодом, имеющим зависимости от указанной внешней системы. Вам все еще понадобятся интеграционные тесты, чтобы доказать, что ваш код работает с реальной, немодированной системой.
Роберт Харви,
8
Иными словами, макет является альтернативой реализации. Вот почему мы в первую очередь запрограммировали интерфейс, чтобы мы могли использовать mocks в качестве замены для реальной реализации. Другими словами, макеты отделены от фактической реализации , а не связаны с ней.
Роберт Харви
3
«Другими словами, тест должен провалиться, если метод не работает, но не потому, что реализация изменилась», это не всегда так. Существует множество обстоятельств, когда вы должны изменить как свою реализацию, так и свои тесты.
whatsisname

Ответы:

4
  1. Я не понимаю, почему издевательства будут нарушать принцип открытого / закрытого. Если бы вы могли объяснить нам, почему вы думаете, что они могут, тогда мы сможем облегчить ваши проблемы.

  2. Единственный недостаток заглушек, о которых я могу подумать, заключается в том, что они обычно требуют больше работы для написания, чем макетов, поскольку каждый из них на самом деле является альтернативной реализацией зависимого интерфейса, поэтому он обычно должен предоставлять полный (или убедительно завершенный) реализация зависимого интерфейса. В качестве экстремального примера, если тестируемая подсистема вызывает СУБД, то макет СУБД будет просто отвечать на конкретные запросы, о которых известно, что они были выданы тестируемой подсистемой, с получением предопределенных наборов тестовых данных. С другой стороны, альтернативной реализацией была бы полноценная СУБД в памяти, возможно, с дополнительным бременем необходимости эмулировать причуды реальной клиент-серверной СУБД, которую вы используете в работе. (К счастью, у нас есть такие вещи, как HSQLDB, так что мы действительно можем это сделать, но все же,

  3. Хорошие сценарии использования для имитации - это когда сложный интерфейс слишком сложен, чтобы написать альтернативную реализацию для него, или если вы уверены, что вы будете писать макет только один раз и никогда не трогать его снова. В этих случаях, идти вперед и использовать быстрый и грязный макет. Следовательно, хорошие сценарии использования для заглушек (альтернативные реализации) - это почти все остальное. Особенно, если вы предвидите вступление в долгосрочные отношения с тестируемой подсистемой, определенно используйте альтернативную реализацию, которая будет хорошей и чистой и потребует обслуживания только в случае изменения интерфейса, вместо того, чтобы требовать обслуживания всякий раз, когда интерфейс изменяется и всякий раз, когда изменяется реализация тестируемой подсистемы.

PS Человек, на которого вы ссылаетесь, мог быть мной, в одном из моих других связанных с тестированием ответов здесь на programmers.stackexchange.com, например, в этом .

Майк Накис
источник
an alternative implementation would be a full-blown in-memory RDBMS- Вам не обязательно заходить так далеко с заглушкой.
Роберт Харви,
@RobertHarvey, с HSQLDB и H2 не так уж и сложно зайти так далеко. Вероятно, труднее сделать что-то наполовину, чтобы не зайти так далеко. Но если вы решите сделать это самостоятельно, вам придется начать с написания парсера SQL. Конечно, вы можете срезать некоторые углы, но впереди много работы . Во всяком случае, как я уже сказал выше, это просто крайний пример.
Майк Накис
9
  1. Принцип Open / Closed в основном заключается в возможности изменить поведение класса без его изменения. Следовательно, введение зависимости от макета компонента в тестируемый класс не нарушает его.

  2. Проблема с тестовыми двойниками (макет / заглушка) заключается в том, что вы в основном делаете произвольные предположения относительно того, как тестируемый класс взаимодействует со своей средой. Если эти ожидания ошибочны, у вас могут возникнуть проблемы после развертывания кода. Если вы можете себе это позволить, протестируйте свой код в рамках тех же ограничений, что и тот, который ограничивает вашу производственную среду. Если вы не можете этого сделать, сделайте как можно меньше предположений и смоделируйте / заглушите только периферийные устройства вашей системы (база данных, служба аутентификации, HTTP-клиент и т. Д.).

Единственная действительная причина, по которой, IMHO, должен использоваться двойник, - это когда вам нужно записать его взаимодействия с тестируемым классом или когда вам нужно предоставить поддельные данные (что могут сделать оба метода). Однако будьте осторожны, злоупотребление им отражает плохой дизайн или тест, который слишком сильно зависит от реализации тестируемого API.

Фрэнсис Тот
источник
6

Примечание: я предполагаю, что вы определяете Mock как «класс без реализации, просто что-то, что вы можете отслеживать», а Stub - как «частичное макетирование, иначе говоря, использует реальное поведение реализованного класса», согласно этому стеку Вопрос переполнения .

Я не уверен, почему вы думаете, что консенсус заключается в использовании заглушек, например, в документации Mockito это как раз наоборот

Как обычно, вы прочтете предупреждение о частичной имитации: объектно-ориентированное программирование более менее эффективно справляется со сложностью, разделяя сложность на отдельные конкретные объекты SRPy. Как частичная имитация вписывается в эту парадигму? Ну, просто нет ... Частичная имитация обычно означает, что сложность была перенесена в другой метод для того же объекта. В большинстве случаев это не тот способ, которым вы хотите разработать свое приложение.

Тем не менее, есть редкие случаи, когда частичные имитации полезны: работая с кодом, который вы не можете легко изменить (сторонние интерфейсы, временный рефакторинг унаследованного кода и т. Д.) Однако я бы не стал использовать частичные имитации для новых, управляемых тестами и хорошо разработан код.

Эта документация говорит это лучше, чем я. Использование mocks позволяет вам просто протестировать один конкретный класс и ничего больше; если вам нужны частичные макеты для достижения искомого поведения, вы, вероятно, сделали что-то не так, нарушаете SRP и т. д., и ваш код может выдержать рефакторинг. Моты не нарушают принцип открытого-закрытого, потому что они все равно используются только в тестах, они не являются реальными изменениями в этом коде. Обычно они генерируются на лету библиотекой вроде cglib.

durron597
источник
2
Из того же поставленного вопроса SO (принятый ответ), это определение Mock / Stub, которое я тоже упоминал: фиктивные объекты используются для определения ожиданий, т. Е. В этом сценарии я ожидаю, что метод A () будет вызываться с такими и такими параметрами. Издевается над записью и проверяет такие ожидания. Заглушки, с другой стороны, имеют другое назначение: они не записывают и не проверяют ожидания, а позволяют нам «заменить» поведение, состояние «поддельного» объекта, чтобы использовать тестовый сценарий ...
Кристофер Франциско
2

Я думаю, что проблема может возникнуть из-за предположения, что единственно допустимые тесты - это тесты, которые соответствуют открытому / закрытому тесту.

Легко видеть, что единственное тестирование, которое должно иметь значение, - это тестирование интерфейса. Однако на самом деле зачастую эффективнее тестировать этот интерфейс, проверяя внутреннюю работу.

Например, почти невозможно протестировать какое-либо отрицательное требование, такое как «реализация не должна бросать никаких исключений». Рассмотрим интерфейс карты, реализуемый с помощью hashmap. Вы хотите быть уверены, что хэш-карта соответствует интерфейсу карты, не выбрасывая, даже когда она должна перефразировать вещи (которые могут быть рискованными). Вы можете проверить каждую комбинацию входов, чтобы убедиться, что они соответствуют требованиям интерфейса, но это может занять больше времени, чем тепловая смерть вселенной. Вместо этого вы немного разбиваете инкапсуляцию и разрабатываете макеты, которые взаимодействуют более плотно, заставляя хэш-карту выполнять именно перефразировку, необходимую для того, чтобы алгоритм перефразировки не генерировал.

Tl / Dr: делать это «по книге» - это хорошо, но когда наступает решающий момент, наличие продукта на столе вашего босса к пятнице более полезно, чем индивидуальный набор тестов, который длится до тепловой смерти Вселенная для подтверждения соответствия.

Корт Аммон
источник