TDD создает несколько классов «контроллера» - на каком уровне должны быть написаны его тесты? - PullRequest
1 голос
/ 15 марта 2012

Я недавно начал практиковать TDD и модульное тестирование, и мои главные учебные пособия - отличный GOOSGBT и прочтение вопросов с тегами TDD здесь, на SO.

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

Первоначально тесты дляклассы будущих контроллеров были написаны на уровне намерений конечных пользователей класса: «Если я сделаю этот вызов, какими должны быть наблюдаемые эффекты, которые меня, конечного пользователя класса, действительно волнуют?».Но по мере того, как все больше обязанностей и тестов для пограничных случаев распределялись по вспомогательным классам (которые заменяются тестовыми двойниками в тестах для класса контроллера), эти тесты стали казаться действительно ... неопределенными и неспецифичными:были неизменно "счастливые пути" тесты, которые на самом деле, казалось, не дошли до сути вопроса.Трудно объяснить, что я имею в виду, но при чтении тестов у меня появилось что-то вроде «Ну и что? Почему вы выбрали именно этот тест« счастливый путь », а не какой-либо другой? Каково значение?Test точно определить точную причину, почему код теперь не работает? "Со временем я все более и более склонялся к тому, чтобы вместо этого написать тесты с точки зрения того, как сотрудники классов должны использоваться вместе: «класс должен вызвать этот метод для этого сотрудника и передать результат этому другомусотрудник ", который дал гораздо более сфокусированный, описательный и четко мотивированный набор тестов (и, как правило, результирующий набор тестов невелик).

Это, очевидно, имеет свои недостатки: тесты теперь сильно связаны со спецификой реализации класса контроллера, а не с гораздо более гибким «что бы конечный пользователь этого класса увидел, что ему не все равнооколо?".Но на самом деле тесты уже достаточно связаны с ним в силу того факта, что они должны настроить всех коллабораторов Test Double так, чтобы они вели себя точно так, как требует реализация, чтобы дать правильные результаты от конечного пользователя классов.точка зрения.

Итак, мой вопрос: находят ли коллеги из TDD, что меньшинство классов мало что делает, а собирает свой (маленький) набор соавторов?Считаете ли вы хранение тестов для таких классов написанным с точки зрения конечных пользователей неточным и неудовлетворительным, и если да, то допустимо ли писать тесты для таких классов явно с точки зрения того, как он вызывает ипередает данные между своими сотрудниками?

Надеюсь, здесь достаточно понятно, к чему я клоню!:)


В качестве конкретного примера: одним из практических проектов, над которым я работал, был загрузчик / просмотрщик телепередач (если вы когда-либо видели «Digiguide», вы будете знать, что яимею в виду), и я реализовывал основную часть приложения - ту, которая фактически обновляет списки по сети и интегрирует недавно загруженные списки в текущий набор известных телевизионных программ.Интерфейсом для этой (на удивление сложной, когда все требования учитывались) функции был класс с именем ListingsUpdater, у которого был метод с именем "updateListings".

Теперь конечные пользователи ListingsUpdater действительно заботятся только о нескольких вещах: после того, как listsUpdate был вызван, корректна ли база данных списков телепередач, и были ли внесены изменения в базу данных (добавление телепрограмм, изменение их при трансляции?) произошли изменения и т. д.) описаны ли слушатели изменений? Когда реализация была очень, очень простой, сделкой типа «притворяйся, пока не сделаешь», это работало нормально: но по мере того, как я постепенно продвигал реализацию к той, которая будет работать в реальном мире, «настоящая работа» стала все дальше и дальше от ListingsUpdater до тех пор, пока он в основном не собрал нескольких соавторов: ListingsRequestPreparer для оценки текущего состояния листингов и создания HTTP-запросов для ListingsDownloader и ListingsIntegrator, который распаковал недавно загруженные листинги и включил их (он тоже делегировал сотрудникам) в базу данных списков. Теперь обратите внимание, что для того, чтобы выполнить контракт ListingsUpdater с точки зрения пользователя, я должен в тесте дать указание его DoubleIntegrator Test Double заполнить (поддельную) базу данных правильными данными (!), Что кажется странным. Гораздо разумнее отбросить тесты «с точки зрения конечного пользователя ListingsUpdater» и вместо этого добавить тест, который говорит, что «когда ListingsDownloader загрузил новые листинги, убедитесь, что они переданы в ListingsIntegrator».

1 Ответ

1 голос
/ 15 марта 2012

Это, очевидно, имеет свои недостатки: тесты теперь сильно связаны со спецификой реализации класса контроллера, а не с гораздо более гибким «что бы конечный пользователь этого класса увидел, что ему не все равнооколо?".Но на самом деле тесты уже достаточно связаны с ним в силу того факта, что они должны настроить всех коллабораторов Test Double так, чтобы они вели себя точно так, как требует реализация, чтобы дать правильные результаты от конечного пользователя классов.точка зрения.

Я повторю то, что я сказал в ответ на другой вопрос :

Мне нужносоздайте для каждой зависимости макет заглушки или фиктивного объекта [тестового двойника]

Об этом обычно говорится.Но я думаю, что это неправильно.Если Car связан с объектом Engine, почему бы не использовать настоящий объект Engine при модульном тестировании вашего Car класса?

Но кто-то объявит, если вы это сделаетене модульное тестирование вашего кода;ваш тест зависит как от Car класса , так и от класса Engine: два модуля, поэтому интеграционный тест, а не модульный тест.Но эти люди тоже издеваются над классом String?Или HashSet<String>?Конечно, нет.Граница между юнит-тестированием и интеграционным тестированием не так ясна.

С точки зрения философии, во многих случаях вы не можете создавать хорошие фиктивные объекты [удвоение теста].Причина в том, что для большинства методов способ делегирования объекта связанным объектам не определен.Будет ли это делегировано, и как, оставлено контрактом в качестве детали реализации.Единственное требование заключается в том, чтобы при делегировании метод удовлетворял предварительным условиям своего делегата.В такой ситуации подойдет только полностью функциональный (не фиктивный) делегат.Если реальный объект проверяет свои предварительные условия, невыполнение предварительного условия при делегировании вызовет сбой теста.И отладка этого неудачного теста будет легкой.


И я добавлю в ответ на

они были неизменно "счастливыми путями", которые на самом деле не казалисьЧтобы понять суть дела

Это более общая проблема тестирования, не относящаяся к TDD или модульному тестированию: как выбрать хороший набор тестовых случаевс учетом того, что комплексное тестирование невозможно?Я полагаюсь на эквивалентное разбиение .Когда я начинаю работать над некоторым кодом, я использую разбиение эквивалентности, чтобы выбрать набор тестовых примеров, которые я хочу передать, а затем по очереди работать с каждым из них в стиле TDD, , но , если передается один изконтрольные примеры не требуют изменения кода (поскольку в начале работы был создан код, который также удовлетворяет этому контрольному примеру), я все же добавляю контрольный пример в мой набор тестов.Поэтому мой набор тестов имеет лучшее покрытие возможных путей ошибок .

...