TDD, как обрабатывать изменение в издеваемом объекте

При написании модульных тестов для каждого объекта, с которым взаимодействует единица, я делаю эти шаги (украденные из моего понимания JBrains Интеграционные тесты - это мошенничество):

  • Напишите тест в блоке, чтобы убедиться, что он отправляет правильный вызов и params сотрудничающему объекту
  • Напишите тест в блоке, который гарантирует, что он обрабатывает все возможные ответы от сотрудничающего объекта. Эти ответы все высмеиваются, поэтому блок тестируется изолированно.
  • Напишите тест в сотрудничающем объекте, чтобы убедиться, что он принимает вызов и параметры.
  • Запишите тесты, чтобы каждый ответ был отправлен обратно.

Мой вопрос возникает, когда я редуцирую рефакторинг объекта с ответами, посмеявшимися на шаге 2. Если я изменю способ ответа объекта на вызов, ни один из тестов, которые другие объекты для этого вызова не сработает, потому что они все издевались над старым стилем. Как вы держите москиты в курсе событий, которые они издеваются? Есть ли лучшая практика для этого? Или я полностью неправильно понял вещи и делаю все это неправильно?

Ответ 1

Я делаю это так.

Предположим, что мне нужно изменить ответы от метода интерфейса foo(). Я собираю все тесты сотрудничества, которые заглушают foo() в списке. Я собираю все контрактные тесты для метода foo(), или если у меня нет контрактных тестов, я собираю все тесты для всех текущих реализаций foo() в списке.

Теперь я создаю ветку управления версиями, потому что на некоторое время будет беспорядочно.

I @Ignore (JUnit speak) или иным образом отключить тесты совместной работы, которые заглушают foo(), и начать повторное внедрение и повторное выполнение их по одному. Я заставляю их всех проходить. Я могу сделать это, не касаясь какой-либо производственной реализации foo().

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

На этом этапе все тесты совместной работы теперь принимают новые ответы от foo(), а тесты тестов/тестирования контрактов теперь ожидают новые ответы от foo(), поэтому It Should All Just Work. (TM)

Теперь интегрируйте свою ветку и налейте себе немного вина.

Ответ 2

Исправленный вариант: Это компромисс. Простота тестирования путем изолирования объекта от его среды vs Уверенность в том, что все это работает, когда все части собраны вместе.

  • Цель стабильных ролей: Подумайте о клиентских ролях (а не о связях методов). Я нашел роли (написанные с точки зрения потребностей клиента/клиент-первый/внешний) менее волатильными. Проверьте, является ли эта роль непротекающей абстракцией, предающей детали реализации. Также смотрите роли, которые являются магнитами изменения (и придумывают план смягчения).
  • Если вам нужно внести изменения, посмотрите, можете ли вы "опираться на компилятор". Компилятор прекрасно помечает такие вещи, как изменение сигнатуры метода. Так что используйте его.
  • Если компилятор не может помочь вам в определении изменений, будьте более прилежными, чем обычно, если вы не пропустили место (использование клиента).
  • Наконец, вы возвращаетесь к приемочным испытаниям, чтобы поймать такие проблемы - убедитесь, что Object A и Collaborators B, C, D воспроизводятся по тем же предположениям (протокол). Если что-то удастся избежать вашего перетаскивания, шансы высоки, по крайней мере, один тест должен определить это.

Ответ 3

Во-первых, это определенно сложнее получить этот уровень охвата интеграционными тестами, поэтому я думаю, что модульные тесты все же выше. Однако, я думаю, у вас есть точка. Трудно сохранить синхронизацию ваших объектов.

Ответ на этот вопрос заключается в том, чтобы иметь частичные интеграционные тесты, которые имеют реальные сервисы на 1 уровне, но кроме того, это издевательства. Например:

var sut = new SubjectUnderTest(new Service1(Mock.Of<Service1A>(), ...), ...);

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

Вы можете решить эту проблему на функциональном языке программирования, используя дискриминированные союзы. Например:

// discriminated union
type ResponseType
| Success
| Fail of string   // takes an argument of type string

// a function
let saveObject x =
    if x = "" then
        Fail "argument was empty"
    else
        // do something
        Success

let result = saveObject arg 

// handle response types
match result with
| Success -> printf "success"
| Fail msg -> printf "Failure: %s" msg

Вы определяете дискриминационный союз с именем ResponseType, который имеет ряд возможных состояний, некоторые из которых могут принимать аргументы и другие метаданные. Каждый раз, когда вы получаете доступ к возвращаемому значению, вам приходится иметь дело с возможными различными условиями. Если вы хотите добавить другой тип отказа или тип успеха, компилятор даст вам предупреждения для каждого раза, когда вы не будете обрабатывать новый член.

Эта концепция может пройти долгий путь к обработке эволюции программы. С#, Java, Ruby и другие языки используют исключения для связи с условиями отказа. Но эти условия неудачи часто не являются "исключительными" обстоятельствами, что в конечном итоге приводит к ситуации, с которой вы имеете дело.

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

Ответ 4

Вы не должны доверять людям (даже самому себе) о том, чтобы синхронизировать макет и реальные программные компоненты.

Я слышал, вы спрашиваете?

Тогда каково ваше предложение?

Мое предложение:

  • Вы должны написать mocks.

  • Вы должны только писать mocks для программных компонентов, которые вы поддерживаете.

  • Если вы поддерживаете программный компонент с другим разработчиком, вы и другой разработчик должны поддерживать mock этого компонента вместе.

  • Вы не должны издеваться над другим компонентом.

  • Когда вы пишете unit test для своего компонента, , вы должны написать отдельный unit test для макета этого компонента. Позвольте называть это MockSynchTest.

  • В MockSynchTest вы должны сравнить каждое поведение макета с реальным компонентом.

  • Когда вы вносите изменения в свой компонент, вы должны запустить MockSynchTest, чтобы убедиться, что вы сделали ваш макет и компонент не синхронизированы или нет.

  • Если вам нужен макет компонента, который вы не поддерживаете при тестировании своих компонентов, спросите разработчика этого компонента о макете. Если она сможет предоставить вам проверенный макет, хорошо для нее и повезет вам. Если она не может, попросите ее следовать этому руководству и предоставить вам проверенный макет.

Таким образом, если вы случайно внесите свой макет из синхронизации, там будет неудачный тестовый пример, чтобы предупредить вас.

Таким образом вам не нужно знать детали реализации внешнего компонента для насмешек.

How-to-write-good-tests#dont-mock-type-you-dont-own