Pull to refresh

Comments 64

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Спасибо
Есть ли книги, которые стоит посоветовать для хорошего понимания TDD?
JUnit Recipes
Practical Methods for Programmer Testing
Где-то я уже вроде видел это, вроде даже в переводе, но не так полно. Спасибо.
По переводу — «The Peeping Tom» вероятно в русском соответствует идиоме «любопытная Варвара»?
Точно! Вылетела эта идиома из головы. Спасибо за напоминание.
UFO just landed and posted this here
Нормательно, мне понравилось ))
спасибо что собрали их как хорошо что есть место куда можно отправить своих друзей учить/читать
Спасибо, ценная инфа и отличный перевод: чувствуется хорошее владение родным языком. Это я вам говорю как структуральный лингвист-любитель :)
Sign up to leave a comment.

Articles

Change theme settings