1 ноября 2008 в 14:07

Анти-паттерны Test Driven Development перевод

Я надеюсь, что как грамотный разрабочик, вы имеете представление о unit-тестировании и сделаете себе в голове пару мысленных отметок о том, чего надо избегать при написании тестов. Знакомьтесь:

Лжец (The Liar)

Unit-тест, который успешно выполняет все кейсы и выглядит работающим правильно, однако при более детальном рассмотрении обнаруживается, что он на самом деле не тестирует то, что должен.

Чрезмерная Инициализация (Excessive Setup)

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

Гигант (Giant)

Unit-тест, который хотя и правильно тестирует приложение, но растекается на тысячи строк кода и содержит слишком много кейсов. Это может служить признаком того, что тестируемая система представляет из себя антипаттерн Всемогущий Объект (God Object).

Подделка (The Mockery)

Mocking можеть быть очень удобным и правильным. Но случается, что разработчики теряют чувство меры и используют его даже для тех частей системы, которые в принципе должны участвовать в тестировании. В этом случае unit-тест содержит так много mocks, заглушек (stubs) и фейков (fakes), что часть системы остается непротестированной.

Инспектор (The Inspector)

Unit-тест, который нарушает инкапсуляцию в попытке достичь 100% покрытия кода (code coverage) и при этом знает слишком много о тестируемой системе. При рефакторинге системы такой тест слишком часто ломается и требует исправлений.

Щедрые Остатки (Generous Leftovers)

Случай, когда один unit-тест создаёт данные, которые где-то сохраняются, а другой тест их потом переиспользует. Если «генератор данных» будет по какой-то причине вызван позже или пропущен, то тест, использующий его данные, не пройдёт.

Местный Герой (The Local Hero)

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

Крохобор (The Nitpicker)

Unit-тест, который проверяет весь результат работы, в то время как на самом деле важна только его малая часть. В результате приходится часто обновлять тест, чтобы отражать изменения в незначительных вещах. Типичен при тестировании веб-приложений.

Тайный Ловец (The Secret Catcher)

Тест, который на первый взгляд не делает никакого тестирования из-за отсутствия assertions, но на самом деле дёргает за ниточки системы и полагается на выбрасывание какого-то исключения в случае проблем. Ожидается, что тестовое окружение поймает эту ошибку и отобразит тест как проваленный.

Уклонист (The Dodger)

Unit-тест, который тестирует множество второстепенных (и, как правило, простых) мелочей, но не тестирует основное поведение.

Крикун (The Loudmouth)

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

Жадный Ловец (The Greedy Catcher)

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

Любитель Порядка (The Sequencer)

Тест, который зависит от того, что фактически неупорядоченные данные всегда появляются в одном и том же порядке.

Скрытая Зависимость (Hidden Dependency)

Близкий родственник Местного Героя. Это unit-тест, который требует, чтобы перед запуском были заполнены какие-то данные. Если эти данные отсутствуют, то тест падает, оставляя мало информации о причине проблемы и заставляя разработчика копаться в груде кода для того, чтобы определить какие данные и откуда должны были взяться.

Счётчик (The Enumerator)

Unit-тест, в котором все кейсы плохо именованы (например, test1, test2, test3). В результате назначение тест-кейса неясно и единственный способ понять, что сломалось — лезть в код теста и молиться, чтобы он оказался понятным.

Чужак (The Stranger)

Кейс, который не относится к unit-тесту, в котором он расположен. Он на самом деле тестирует совершенно другой объект, чаще всего объект, который используется основным тестируемым объектом. Также известен как Дальний Родственник.

Приверженец ОС (The Operating System Evangelist)

Unit-тест, который полагается на особенности определённой операционной системы. Хорошим примером будет тест, который ожидает перевода строки, принятого в Windows и ломающийся, когда выполняется под Linux.

Успех Любой Ценой (Success Against All Odds)

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

«Заяц» (The Free Ride)

Вместо того, чтобы написать новый кейс-метод, просто добавляется новый assert к существующему кейсу.

Избранный (The One)

Комбинация нескольких анти-паттернов, в особенности «Зайца» и Гиганта. Такой unit-тест состоит из единственного метода, который тестирует всю функциональность объета. Типичным индикатором проблемы являтся название тестового метода по названию unit-теста и большое количество строк инициализации и assert-ов.

Подглядыватель (The Peeping Tom)

Тест, который из-за общих ресурсов может видеть данные других тестов и может упасть, даже если тестируемая система полностью валидна. Типичным примером может служить использование статических полей для хранения коллекций. Если они не очищаются должным образом, то возможны неожиданные побочные эффекты в других тестах. Также известен как анти-паттерн Незваные Гости.

Тормоз (The Slow Poke)

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

От переводчика: восхищения умными мыслями — автору, пинки за перевод — мне. :)
Автор оригинала: James Carr
Lite @Lite
карма
122,2
рейтинг 0,0
Самое читаемое Разработка

Комментарии (64)

  • 0
    Спасибо.
  • 0
    Спасибо! Очень интересно!
  • +2
    Инспектор (The Inspector) — пожалуй самый неприятный момент в тестировании.
  • 0
    Имея некоторый опыт в TDD, я бы предложил сосредотачиваться на паттернах, а проще говоря на рекомендация о том, как НАДО писать тесты.

    И не придумывать дурацкие названия антипаттернам — мне трудно представить, что в лексиконе разработчиков появляются фразы вроде «Тест Успех любой ценой», «Тест Жадный ловец» и т.п.
    • +5
      Знание паттернов (как надо) и анти-паттернов (как не надо), по-моему, очень хорошо дополняют друг друга. Я и сам предпочитаю более строгие названия, но перевод есть перевод. Кроме того, специально оставил первоначальные названия на английском, которые субъективно воспринимаются чуть более серьёзно. ;)
  • 0
    Хочется сделать комплимент и автору и Вам: офигенные названия паттернов и отличный перевод!

    Жаль, сложно держать все анти-паттерны в голове…
    • 0
      С опытом это приходит на интуитивном уровне.
  • 0
    Очень толково расписаны большинство потенциальных «вил» в test-driven development. Неплохой перевод — спасибо за пост!
  • 0
    Было бы неплохо, если бы мы обсудили здесь паттерны/анти-паттерны проектирования, которые мешают написанию правильных тестов. Начну первым.

    1) Singleton — ужаснейший анти-паттерн в тестировании и разработке. Чрезмерное использование синглетонов в проекте — очень… очень плохое решение.

    2) «Недоношенный» обьект — когда после вызова конструктора по-прежнему необходима допололнительная инициализация… в виде вызовов init(), start(), polulate(...) Это всё Гнилой код.

    Продолжаем список…
    • +2
      а что рекомендуется вместо синглтона? когда нужен один объект глобальный? :)
      • 0
        Я не согласен что синглтон анти-паттерн, но вполне представляю что его можно заменить например так:

        при использовании синглтона мы пишем допустим так DownloadManager.Instance.Progress, чтобы получить прогресс в нашем методе

        без синглтона тоже самое можно достичь следующим образом:
        создаем экземпляр DownloadManager и подсовываем его в некоторые сервисные функции Services.GetDownloadProgress(downloadManager), то есть один и тот же объект кочует по сервисам итд

        Но это ИМХО!
        • 0
          создается кем? по сути всё равно должен быть некий «рулящий» объект, сделанный тем же синглтоном, но раздающий определенные объекты по запросам (а-ля фабрика объектов). но от синглтона тут тоже не уйти :)
          • +2
            Нет, от синглтона мы полностью уходим, потому что создаем один экземпляр объетка в сборке и передаем его во все методы или классы где хотим его использовать.

            допустим есть класс которому для работы нужно соединение с базой

            с синглтоном:
            у нас есть класс MyClass у которого конструктор без параметров, и внутри работы он юзает синглтон

            без синглтона:
            у нас есть класс MyClass, но конструктор у него принимает параметором соединение с базой, и внутри работы использует уже его

            • 0
              когда таких классов 2-3 десятка, и каждому надо по 2-3 синглтона… я сочувствую архитектору такой системы :)
              и ответ я так и не увидел — где создается один экземпляр объекта?
              • 0
                ну в любом мест сборки создаешь перед созданием классов где будешь использовать этот объект

                я сам не против синглтонов если вмеру
              • +2
                Единственный экземпляр самому создавать не надо. Он либо уже у вас есть(пришел в качестве параметра конструктора или метода — Dependency Injection), либо вы его получаете через посредника (Service Locator).
                • 0
                  а Service Locator у нас не синглтон ли случайно? если нет — то что это за объект?
                • 0
                  какая разница что я передам объект через конструктор или запрошу его же из некоего реестра где хранятся объекты?

                  если я буду передавать объекты базы данных например через конструктор, а потом кто-то после меня решит это дело поправить — сколько же ему кода придется перелопатить чтобы везде изменить ?:)
                • +1
                  Тем самым мы увеличим связаность, что тоже не гуд. Да и как быть с ленивой инициализацией, например тоже подключение к бд может и непонадобится, если всё уже есть в кеше.
                  Чрезмерное избегание некоторых антипатернов ведёт к ещё большему засорению кода.
                  С опытом приходит понимание, что антипатерны и денормализация — это не всегда зло.
                  • 0
                    Ну у симфони в их компоненте dependency injection проблема ленивой авторизации решается. У вас есть некоторый объект Context, который передаётся в конструкторы и хранится внутри объекта. Когда вам понадобится класс бд вызываете метод Context->getDBClass, например, и именно тогда создаётся экземпляр класса бд. При повторном вызове метода естественно возвращается ссылка на тот же экземпляр что и раньше.
                    • 0
                      А это, случаем, не вариант синглтона ли? Так как в рамках Context подключение будет уникальным(единственным) объектом, и в рамках Context мы его можем везде получить, или там несколько иначе все устроено?
          • +1
            смысл понятия «анти-паттерн» вовсе не в том, что он однозначно вреден и должен изничтожаться. «анти-паттерны» обычно терпимы в единичных случаях и представляют опасность, когда их много и архитектура приложения серьезно на них опирается.

            конкретно касаемо синглотона: picocontainer.org/singleton-antipattern.html
            • 0
              вот с этим согласен полностью.
              возмутило определение «ужаснейший» в исходном посте.
    • +1
      Хотелось бы поспорить на счет «недоношенного» объекта. У меня как раз есть такой =)

      В моем случае — это сложный десктопный (не веб) элемент управления, взаимодействующий с внешними источниками данных и другим окружением на форме. Критическими для его работы являются около 20 свойств, если они не назначены, или назначены неправильно, элемент работать не может (падает он при этом, или просто отказывается работать — другой вопрос). Если делать этот элемент «самозапускаемым», то процедура назначения каждого свойства должна будет инициировать проверку полноты и целостности всего комплекта свойств. Вил не будет, зато будут грабли. Т.к. придется писать и тестировать отдельный кусок кода, который сможет оказаться нереентерабельным, чувствительным к порядку назначения свойств или еще что-нибудь…
      Уж лучше, на мой взгляд, в этой ситуации оставить метод Activate(), который программист гарантированно вызовет только после того, как все свойства правильно назначены.
      • 0
        Можно и попорить =)

        Расскажите-ка нам по-подробнее о процедуре тестирования вашего «недоношенного» объекта с 20 свойствами, взаимодействующего с внешними источниками данных и другим окружением на форме? Хотелось бы узнать, как выглядят тесты для такого монстра?
        • 0
          К сожалению, не смогу порадовать какой-нибудь интересной новой методикой, т.к. у нас не индустриальное программирование, а программирование несколько «домашнее».

          Упор сделан не на функциональное тестирование, а на структурное. Мы конечно не привлекали теорию алгоритмов в явном виде и не рисовали полный конечный автомат состояний объекта, но очень много времени уделили разрисовке вариантов использования. Тестированием занимались те же люди, что и разработкой (хотя теория тестирования этого делать не рекомендует), поэтому тестирующие понимали наиболее опасные места и все варианты развития событий и внимательно по ним прошлись.
          А поскольку описываемый мною класс является элементом управления, правильность его работы видна «на глазок».
      • 0
        Добавлю, что обилие классов и/или объектов, которые требуют особого метода активации действительно может внести неразбериху в код, но если этот метод будет только у одного-двух ключевых объектов, на задействовании которых все равно сосредоточена значительная часть внимания разработчика, то запуск методом не создает заметных неудобств.

        Более того, я могу себе позволить определенные программистические вольности, пока объект уже создан, но еще не активирован.
    • 0
      Поясните, пожалуйста, почему 2) плох именно для тестов.
      • 0
        Второй подход плох в принципе… init() метод можно 1) забыть вызвать, можно 2) вызвать 2жды, а можно и 3) не успеть вызвать(в моногопоточной среде).

        Обьект должен рождаться «полноценным» либо «мертвым»(с выбросом исключения). Это упрощает процедуру инициализации приложения и уменьшает количество тесткейсов(вам не придётся писать тесты для случаев 1,2,3).
        • 0
          Я согласен, что использовать класс с методом-активатором менее удобно и безопасно, чем класс, который вступает в работу сам.
          Но с другой стороны недостатки 1), 2) и 3) преувеличены.
          1) Можно забыть выполнить любое действие, от инициализации локальной переменной до бэкапа корпоративной БД.
          2) Можно любое действие случайно выполнить два раза )
          3) А можно и сам объект не успеть создать… нужно вдумчиво подойти к планированию межпоточных блокировок. Полагаться на упорядоченность асинхронных действий все равно никогда нельзя. Это же, так называемые «состязания», которые потенциально опасны с точки зрения правильности работы.

          Ну вот как родить полноценный объект? В моем случае? Конструктор с 20 параметрами делать что ли.

          Кроме того, сама идеология написания и использования пользовательских элементов управления предписывает вызов конструктора класса без параметров, а потом назначение ему всех свойств в (условно) произвольном порядке. Как объект поймет, что его уже «запрограммировали»?
          А если давать ему пытаться запуститься при каждом изменении ключевого слова, то процедура активации (которая все равно будет присутствовать в классе, только не открытая, а закрытая) будет пестреть IsNothing'ами, <>0, <>"" и прочими проверялками введенности переменных.
          • 0
            Попробуйте разделить ваш обьект на несколько более мелких частей. Часть свойств можно комбинировать. А ля…

            new MyObject(Panel parent, MyObject.STYLE_FLAT|MyObject.STYLE_RIGHT|MyObject.COLOR_RED).

            Это первое, что пришло в голову. Дальше нужно знать специфику вашего контрола =))
            • 0
              А мы и разделили. Этот элемент управления имеет внутри себя еще собственную иерархию объектов, включая и пользовательские (наши же). Но все равно на него ложится много работы.

              Скрывать нечего — это элемент для отображения ленточных форм данных (как в Access) (т.е. это довольно нестандарный объект, к тому же очень влиятельный чувствительный к происходящему на форме одновременно).

              Свойства можно комбинировать, можно их группировать в структуры, но это не спасет от явного их указания и более высоких требований к их подготовке.
    • +1
      > 2) «Недоношенный» обьект — когда после вызова конструктора по-прежнему необходима допололнительная > инициализация… в виде вызовов init(), start(), polulate(...) Это всё Гнилой код.

      Скажите это программистам на С++ (а как же эксепшены в конструкторах?), или программистам на Embedded C++ (что такое конструктор? :-D) и они вам скажут, насколько вы заблуждаетесь.
      • 0
        Могу сказать и С++ программистам (простите меня, что о вас совсем забыл). Правда, что бы перефразировать сказанное, мне придётся немного подучить конструкторы в С++ =)).

        Может сами попробуете переформулировать пункт №2 в терминах С++? Было бы здорово =))

        • 0
          Подробности тут: habrahabr.ru/blogs/development/43761/#comment_1089228

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

          Конечно же в Java все не так, там если что gc сам подчистит.
      • +1
        А какие проблемы у С++ с исключениями в конструкторах? RAII тот же на них построен.

        Насчёт Embedded не знаю, но если там нет конструкторов то интересно почему, это же по сути обычный вызов функции, в чём там проблема?
        • +1
          Нет, проблемы огромные и RAII здесь не причем.
          Объект считается созданным полностью тогда, когда отработал его конструктор. Деструктор объекта вызывается только в том случае, когда объект полностью создан.

          Итого, если объект создан на стеке, то при исключении в конструкторе, деструктор вызван не будет. И если вы выделяли какие-то ресурсы в конструкторе, а потом произошло исключение, то деструктор вызван не будет, следовательно произойдет утечка ресурсов.

          Если объект был создан на куче, то возникает проблема не только с вызовом деструктора, но и с освобождением памяти.

          try {
          MyType *myObj = new MyType(); // исключение
          catch(...)
          {
          // обработали исключение
          }

          // чему будет равен myObj и как его тут удалить?

          Ответ — никак. Память неявно должен высвобождать компилятор, на деле он этого не делает.

          В Embedded отсутствует placement new как класс, а глобальный оператор new перегружен, потому что аллокаторы стандартной библиотеки не подходят до случая embedded (к примеру если дело происходит в ядре).
          • +1
            Тем, не менее, можно выбросить какое-нибудь фатальное исключение, которое точно завершит программу, и проблема с памятью будет неактуальтной. Или принять принцип — *все* объекты должны иметь метод init(). Но не так, что одни —надо инициализировать, а другие нет, это порождает путаницу. А вообще по ходу, это серьезный недостаток С++, и каждый изобретает тут свои шрабли, печально.
            • +1
              Закономерный вопрос «Где выбросить-то»? Любое «фатальное» исключение можно словить, если уж не стандартными средствами, то SEH'ом. И вот тут я не вижу недостатков С++. Они конечно же в нем есть и в большом кол-ве, но это тема для отдельного разговора.

              А вообще, подобные вещи, как пункт 2 — слишком специфичны для конкретной ситуации. И нельзя сказать, что делать метод init() для инициализации объекта — это дурной тон. Не сколько не дурной, главное соблюдать единый стиль, чтобы не было путаницы.
      • +1
        эээ А как насчет шаблона Factory или подобного для «недоношенных» объектов?
    • 0
      Эээ… бороться с антипаттернами — хорошо, но что делать, когда синглтон нужен? Например — класс для доступа к БД, для записи в DebugLog, ведь эти классы могут понадобиться в любом месте, и что делать? передавать что ли экземпляры каждому (!) объекту в конструкторе, или как?
      • 0
        Передавать некоторый service container у которого можно получить экземпляр класса бд. Те вместо многих синглтонов один service container. И его передавать в конструктор, либо самого сделать синглтоном.
    • 0
      Мне кажется вам, или кому-то следует оформить отдельную статью, про анти-паттерны, написать известные вам, попросить дополнять в комментариях, и вносить их по мере добавления в статью, чтобы людям, котоыре хотят получить данную информацию, было проще найти то, что нужно.
  • 0
    Спасибо, интересная статья!
  • –1
    Очень интерестно и полезно, спасибо!
    Надеюсь на продолжение темы анти-паттернов.
  • –1
    Отличная статья! Хороший перевод :)

    (убежал навешивать ярлыки на свои тесты...)
  • 0
    Что мне всегда нравилось антипаттернах, так это их названия. Звучат как музыка:)
  • +3
    Это из какой рпг персонажи описаны? ;)
  • 0
    Вот! Вот что меня терзало в TDD! Я чувствовал в нем саму возможность таких антипаттернов и поэтому понимал возможности ошибок и как следствие посчитал тестирование неэффективным. Особенно лень было править тесты при каждом рефакторинге системы.

    • 0
      Тогда вообще лучше не программировать — антипаттернов программирования куда больше :)

      А в чем сложности с правкой тестов при рефакторинге? Рефакторинг по определению не должен менять поведение системы, значит, тесты обновлять не сложнее, чем обычный код. А если рефакторинг проводится автоматическими средствами, то тем более.

      Сложности как раз может вызывать антипаттерн «Инспектор», но на то он и анти.
      • 0
        Да, но за хорошими примерами паттернов программирования и примерами хорошего кода далеко ходить не нужно. А где хорошие паттерны тестирования? Ссылки приветствуются.
  • +2
    Мой любимый — заяц.

    А автору неплохо бы сделать древовидную компоновку, а то простыня из десятков ошибок быстро вылетает из головы.
  • 0
    Что то в этом есть поэтическое
    Похоже
    «Падающий лист при рассвете в зимнее утро»… типа этого :)

    Спасибо
  • 0
    Есть ли книги, которые стоит посоветовать для хорошего понимания TDD?
  • +1
    Где-то я уже вроде видел это, вроде даже в переводе, но не так полно. Спасибо.
    По переводу — «The Peeping Tom» вероятно в русском соответствует идиоме «любопытная Варвара»?
    • 0
      Точно! Вылетела эта идиома из головы. Спасибо за напоминание.
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Нормательно, мне понравилось ))
  • 0
    спасибо что собрали их как хорошо что есть место куда можно отправить своих друзей учить/читать
  • 0
    Спасибо, ценная инфа и отличный перевод: чувствуется хорошее владение родным языком. Это я вам говорю как структуральный лингвист-любитель :)
  • 0
    Каталог антипаттернов xUnit тестирования в книге Effective Unit Testing.
    www.manning.com/koskela2/

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