понедельник, января 21, 2008

TDD Anti-patterns от Джеймса Карра

Джеймс Карр (James Carr) в своем блоге опубликовал пост "TDD Anti-patterns" с кратким перечнем типичных ошибок допускаемых при написании модульных тестов. Ошибки. характерные не только для TDD, а для модульных тестов в принципе. Список показался мне настолько интересным, что я позволю себе представить его в своем вольном пересказе на русском.

Анти-патерны модульного тестирования.

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

Попробуй запусти! (Excessive Setup). Тест, который требует сложной и длительной настройки тестового окружения перед запуском. Иногда тест содержит сотни строк кода, подготавливающие необходимые условия для запуска одного теста. В результате очень сложно разобраться действительно тест не прошел, либо что-то случилось с настройками.

Гигант (The Giant). Огромный тест выполняющий сотни проверок и содержащий множество тест кейсов. Обычно это признак того, что мы имеем дело с God Object

Потемкинские деревни (The Mockery). Модульный тест содержит такое количество моков (mocks) стабов (stubs) и фэйков (fakes), что в самом тесте проверяется не столько тестируемая система, сколько поведение самих моков стабов и результаты их работы.

Инспектор (The Inspector). Модульный тест готов изнасиловать тестируемый класс в попытках добраться до самых приватных его членов, чтобы обеспечить 100% покрытие кода. Любая попытка рефакторинга тестируемого класса приводит к переделке теста.

Щедрые подачки (Generous Leftovers). Ситуация, когда один тест генерирует тестовые данные и сохраняет их где либо, а другой тест пытается этими данными воспользоваться. В результате если первый тест исполняется позже второго или не исполняется вовсе то второй валится.

Местный герой (The Local Hero). Тест написан так, что исполняется только в конкретном окружении, например, только на машине разработчика. Попытка выполнить тест в другом окружении (на сервере сборки, к примеру) ведет к неудаче.

Педант (The Nitpicker). Тест проверяет по шаблону весь выходной поток данных и валится при малейших изменениях в нем, в то время, как значимой для тестирования является лишь небольшая часть данных. Типичный обитатель тестов web приложений.

Тайнос агентос (The Secret Catcher). Тест на первый взгляд ничего не проверяет. На самом деле, тест рассчитывает на то, что при его исполнении не возникнет определенное исключение. А уж если оно возникнет, то тестовый фреймворк отрапортует об этом.

Лентяй (шланг) (The Dodger). Тест который тестирует множество побочных эффектов тестового случая вместо того чтобы тестировать требуемое поведение (обычно из-за того что побочные эффекты протестировать легче).

Болтун (The Loudmouth). Модульный тест заваливает консоль диагностическими сообщениями, логами и прочей чепухой, даже в случае успешного прохождения. Обычно это следствие того, что разработчик отлаживал свой код при помощи этого теста, однако не удалил свои отладочные сообщения после того, как они стали не нужны.

Проглот (The Greedy Catcher). Тест, который проглатывает исключения в исходном коде, иногда вместе с трассировкой стека, заменяя их своими, менее информативными исключениями, либо вовсе выбрасывая диагностическое сообщение (см. Болтун) и завершаясь успешно.

Секвенсор (The Sequencer). Тест, который проявляет чувствительность к последовательности элементов несортированного списка (либо, например, к последовательности выполнения тестов).

Скрытая зависимость (Hidden Dependency). Близкий родственник «Местного героя». Тест, который проявляет зависимость от данных которые должны быть созданы где-то и кем-то до того как тест будет исполнен. Если нужных данных нет, тест валится с каким нибудь невнятным сообщением без всякого намека на то, что же ему нужно было для успешного завершения. И правильно, заставим их продираться через гектары кода в поисках того, что же нужно этому тесту для работы.

Счетчик (The Enumerator). Тест, каждый метод в котором именуется просто: test1, test2, test3 и т.д. В результате абсолютно не понятно, что где тестируется и единственный способ узнать это – смотреть код теста.

Посторонний (The Stranger). Тестовый метод, который тестирует что-то, не имеющее никакого отношения к тому, что тестирует остальной модульный тест. Иногда это случается из-за того что надо протестировать другой объект, связанный с тестируемым объектом, но делается это в отрыве от означенной связи.

ОС евангелист (The Operating System Evangelist). Тест, который полагается на то, что будет исполняться под конкретной операционной системой. Хорошим примером может служить тест, использующий в асерте последовательность символов перевода строки для Windows, он свалится при исполнении в Linux.

Всем врагам на зло (Success Against All Odds). Тест написанный по принципу «pass first» вместо «fail first», т.е. он пройдет успешно даже если тестируемого класса еще нет в природе (см. также "Лжец").

Заяц (The Free Ride) Вместо того, чтобы писать новый тестовый метод, добавляем еще один асерт к уже существующему. И все дела…

Одиночка (The One). Комбинация нескольких паттернов, особенно «Гиганта» и множества «Зайцев». Тест содержит всего один метод, который тестирует всю функциональность тестируемого класса. Характерный признак – тестовый метод имеет такое же название как и тестовый класс, а в самом методе сначала долгая настройка а затем куча асертов.

Любопытная Варвара (The Peeping Tom). Тест, который благодаря разделяемым ресурсам, может «подсмотреть» результаты других тестов, и вследствие этого свалиться, несмотря на то, что тестируемая система находится в валидном состоянии. Часто такая ситуация возникает при использовании статических переменных для хранения данных, которые не были очищены предыдущими тестами. Также известен под именем «Незваный гость».

Не дождетесь! (Копуша) (The Slow Poke). Тест, который исполняется необычайно медленно. Когда разработчик запускает такой тест, он может спокойно сходить в туалет, перекурить, или еще хуже когда он предпочитает запускать такой тест в конце рабочего дня перед уходом домой.

5 комментариев:

Elena Makurochkina комментирует...

Согласна почти со всем, кроме...

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

Еще бывают полезны Hidden Dependency. Это хороший способ избавления от The Giant + Excessive Setup = The Slow Poke. Когда реально для моделирования ситуации требуется большой объем настройки тестового окружения. Если разносить все в отдельные тесты, где окружение настраивается каждый раз заново (и каждый раз с большим количеством проверок самих настроек), то тесты становятся большими, слабочитаемыми и слабоконтролируемыми, и обычно медленными в исполнении. Рецепт: выделить набор тестов в отдельный тестовый блок, где они всегда выполняются в нужной последовательности, единожды настроить окружение и в каждом следующем тесте использовать результаты предыдущего.

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

PS: У меня недописанный пост по тестированию (юнит и функциональные тесты) с начала лета лежит, но появление бэбика абсолютно выбило из колеи и довести текст до читаемого состояния так и не успела. Надеюсь в обозримом будущем появятся силы на блог.

Sergey Rozovik комментирует...

Карр явно указывает, почему он занес "Инспектор" в анти-патерны: он существенно затрудняет рефакторинг тестируемого класса. Это давняя дискуссия - могут ли и должны ли модульные тесты нарушать инкапсуляцию тестируемых классов. Я считаю что нет. Тесты должны тестировать внешний интерфейс классов, на то он и ООП, чтоб скрывать (инкапсулировать) внутреннюю логику объектов. Что касается описанного вами, Елена, случая, то тут надо рефакторить дизайн тестируемого модуля. Удобство тестирования это один из аспектов, которые следует учитывать при дизайне.

Второй поинт. Большой объем настроек для тестового окружения - тоже известная проблема. И она порождает соблазн построить единый тестовый набор данных для всех тестов или использовать выходные данные одного теста на входе другого. Увы решая одни проблеммы мы таким образом множим другие. Атомарность и независимость модульных тестов - это фундаментальный принцип, который мы нарушаем. Это порождает массу проблем и результат - тесты становится очень сложно поддерживать, их забрасывают и остаются вовсе без тестов.

Sergey Rozovik комментирует...

to Elena Makurochkina
>но появление бэбика абсолютно выбило из колеи и довести текст до читаемого состояния так и не успела.

Поздравляю! Растите большие и здоровые! :))

Iv комментирует...

Отлично, спасибо за статью.

Sergey Rozovik комментирует...

to Iv

Рад стараться :)