Как стать автором
Обновить

Комментарии 89

Да, они появляются, но это плюс, а не минус. Да, они замедляют код исполнение кода, но упрощают его понимание и модернизацию. Причем замедляют не на порядки (если без фанатизма), а на проценты. А упрощают, имхо, минимум в разы, опять-таки, если без фанатизма.
Дело в том, что само по себе юнит тестирование не говорит о том, какой unit можно брать для тестирования. Часто под этим понимают класс, но это может быть как отдельная функция, так и целый модуль. Если требуется производительность, то стоит подумать о том, чтобы тестировать модули как целое. Тут главное, применять эти принципы без фанатизма, а так, чтобы было действительно удобно и способствовало тестированию и выявлению ошибок на ранних стадиях. В нашем проекте большинство юнит тестов написано именно на модули, а не на отдельные мелкие сущности. И могу сказать, что производительность была на первом месте, и она не пострадала. Дополнительные методы — да, но они никоим образом не нарушали инкапсуляцию.
На русский язык unit test переводится как модульный тест, что намекает :)
Я думаю, что десяток ассемблерных команда jmp (в которую в итоге превратиться вызов метода класса или интерфейса) увеличат скорость обработки информации на те же 10-20 тактов процессора. Процессоры даже уровня pentium 3 обработают это даже не заметив разницы. А вот предупреждение 10-20 ошибок, которые не появились благодаря TDD могут сэкономить от 10 до 10 000$.
Нужно всё-таки различать сложно архитектуры серверных приложений и сложность архитектуры микро-контроллерных приложений, где каждый процессоры слабее, а памяти меньше, зато архитектура проще.
исправить: "… увеличат время обработки информации на те же 10-20тактов"
При моем небольшом опыте использования TDD заметил:
0) «Затык» в написании теста как правило сигнал о том, что не совсем хорошо написал АПИ
1) Это отличная возможность подумать над интерфейсом API на начальном этапе и его удобстве использования
2) Не следует писать много тестов, нужно писать умный тест, даже если придется задуматься на минуту по-дольше
3) Довольно часто хочется знать ответ на вопрос: А не сломал ли я то что у меня уже было?
4) Легко разделяет команду на тестер \ разработчик и также быстро происходит смена ролей
>>4) Легко разделяет команду на тестер \ разработчик и также быстро происходит смена ролей
Это особенно полезно при Agile-разработке
В agile разработке тесты (модульные) должен писать разработчик, а не тестировщик.
3) В умном тесте проще ошибиться. Тесты на тесты?
Согласен. Тест не должен быть умным, он должен быть простым как 2 копейки и тестировать вешь, никаких умных тестов.
*тестировать одну вещь
Прежде чем ответить на ваш вопрос поясню что я имел ввиду под словом «умный». Прежде всего надо понимать что тест-метод это прежде всего мини-программа и как всякая программа он должен иметь строгу одну цель ради которой разработан. Но вот на практике оказывается, что также достигается и множество других целей не связанных с основной. Допустим основная цель: «протестить класс по работе с атрибутами файлов», но в результате не явно происходит открытие чего-нить другого, создание чего-нибудь еще и др. Так вот когда мы релизовываем мы условно закладываемся на то что и те, другие вспомогательные объекты тоже хорошо работают иначе тест провален. Поэтому мое мнение таково: т.к. тест-метод это все же не коммерческий продукт мы имеем возможность написать избыточную реализацию, чтобы благодаря этой избыточности можно было бы неявным образом захватить работу и других классов\функций\механизмов. Конечно же нужно понимать что тест-метод должен срабатывать более менее быстро.
Так вот «умный» тест-метод в моем понимании содержит такой код, который позволяет неявным образом больше затестить. Для меня это важно, т.к. в основном тесте для какой-нить функкции я могу забыть что-нить проверить
Ну, это получаются интеграционные или функциональные тесты. Основа TDD юнит-тесты, они должны по определению тестировать только одну сущность.

А чтобы не забыть что-то протестировать, нужно писать тесты до реализации. И единственной (в идеале) причиной писать код должен быть неработающий тест. Как только код, достаточный для выполнения теста написан, то больше писать ничего не нужно, кроме следующего теста. Таким образом непротестированного кода не будет.
Тесты, содержащие больше одного ассерта, — это, обычно, плохой запах.
На практике у меня это как-то плохо получается ) Возьму к примеру функцию strip() назначение которой удалить пробельные символы по бокам. При проектировании тест-метода я представляю эту функцию неким магическим черным ящиком работу которого не знаю, но знаю для чего нужен и что должен выдать. Меня интересуют след. ситуации:

1) Что будет если подать пустую строку?
2) Что будет если подать смешанное к примеру один пробел и один таб, либо 2 таба потом 3 пробела и снова таб?
3) Что будет если с одной стороны нет пробельных символов, а с другой есть?

Уже три утверждения которые нужно проверить. Если правильно Вас понял то вы бы на каждый из этих случаев написали бы по тест-методу. Все верно?

В любом случае, скажу что мне куда ближе формулировка:

«Функция strip() на строке работает успешно»

чем формулировки:
* «Функция strip() с пустой строки работает хорошо»
* «Функция strip() на смешанных пробельных символах работает успешно»
* «Функция strip() на пробельных символах с одной из сторон работает успешно»

По факту при использовании функции мне хочется знать ответ «Работает она или нет?» и мне по сути без разницы на какой из перечисленных выше ситуаций она не работает, главное сейчас для меня, что доверять нельзя!
Вы все отлично расписали, только в своем примере вы тестируете готовую функцию — это не совсем TDD. Представьте, что вы только хотите ее написать. Ставите требование:
1. strip() на пустую строку должна вернуть пустую строку
Пишите тест shouldReturnUndefOnEmptyString. Тест проваливается, пишите код, который заставляет тест проходить. Дальше новое требование:
2. strip() на смешанных пробельных должна вернуть строку без пробелов.
Опять тест, красная полоска, код, зеленая полоска.
3. strip() на пробельных символах с одной из сторон должна вернуть строку без пробелов.
Опять то же самое :)

На выходе получается 3 маленьких теста, которые по сути являются спецификацией вашей функции strip. Глядя только на названия вы получаете огромное количество информации (ведь тесты должны выступать и в роли документации). Сравните это с одним тестом testSuccess.

Помимо вышеперечисленного, такой подход решает проблему первого теста, делает тесты менее хрупкими, позволяет с легкостью наращивать функционал не модифицируя старые тесты, устраняет дублирование в тестах. В общем рекомендую попробовать, через два дня будет сложно представить, как можно было раньше писать иначе :) Заодно сделаете большой шаг в сторону BDD.
>>Сравните это с одним тестом testSuccess
1) Название тест-метода не testSuccess, а testStrip хотя бы ;)
2) Никто не мешает указывать текст утверждения при вызове assert-функции! Вы по прежнему будете знать где и что отвалилось? Но при этом код будет компактней!
Можете поделиться опытом написания tearDown(), setUp() методов?
Возьмем тот же самый пример с тестированием strip() функции. По нормам и правилам TDD мы должны отделить создание тестовых строк от самого тест-метода и написать их в setUp() методе. Однако! Все эти три ситуации требуют трех разных строк s1, s2, s3. создание которых будет каждый раз повторяться при вызове очередного тест-метода. Это несколько напрягает.
На мой взгляд куда правильней иметь возможность создать индивидуальный setUp-метод для каждого тест-метода.
В самом начале нашего TDD пути инициализацию для тестов старались унифицировать и засунуть в setup. Недостатки очевидны: низкая скорость, нечитабельные тесты (при чтении теста приходится постоянно заглядывать в setup), сам сетап перегружен и сложен в понимании.

Сейчас мы практикуем принцип трех AAA — Arrange Act Assert (пересказывать не буду, можно почитать тут), т.е. в каждом тестовом методе вся инициализация и создание фикстур выделены в отдельный блок (arrange ). Если замечаем дублирование, просто выделяем отдельный метод и вызываем его во всех нужных тестах. Таким образом тест обычно выглядит следующим образом:

sub shouldSetMessageOnFailed
{
...

# Arrange
my $transaction = $self->_buildFailingTransaction();

# Act
$transaction->process();

# Assert
$self->assert_equals('some error text', $transaction->get('message'));

}


В примере _buildFailingTransaction может явно вызываться в любых тестах. Еще удобно использовать паттерн ObjectMother, но это отдельная история.

К слову, в rspec setup и teardown можно писать для группы методов (еще и используя вложенность). Приведенный выше пример — практически то же самое, только без сахара.
в таких ситуациях часто получаются одинаковые тесты, за исключением одной-двух строк
копипаста плоха в любом коде, даже если это тест
Смотрите комментарий выше про AAA. Act и assert уникальные для каждого теста. Arrange (создание фикстур, инициализация) выделяются в отдельные методы либо самого теста, либо, при частом использовании, в Mother Object. Таким образом устраняется дублирование, а тесты становятся очень лаконичными, обычно меньше 10 строк, и понятными (пример выше).

Что касается дублирования в целом: TDD/BDD гуру утверждают, что в тестах очень важна читабельность и пытаться полностью устранить дублирование, жертвуя читабельностью, затея не самая оправданная. Тут, как и везде, нужен баланс.
Довольно часто хочется знать ответ на вопрос: А не сломал ли я то что у меня уже было?

Лично для меня это главный повод использовать TDD. Иногда после каких-либо изменений старого кода находятся места, падающие от этих изменений. И лучше прогнать проект через тесты, чем отправлять на релиз багнутый код.
Формально, для этого просто нужно покрытие тестами, проектирование с оглядкой на возможность тестирования и т. п. TDD как таковая не нужна, разве как дисциплинирующая методика, не позволяющая откладывать написание тестов на потом.
Вопрос, заданный техдиром автору, в общем-то, касается скорее тестов как таковых, нежели TDD. А так да, дисциплинирующая методика.
По-моему, речь идёт именно о TDD. «Лишние» интерфейсы и слои появляются как раз при TDD. При простом покрытии уже существующего кода тестами ничего там не появляется :)
Еще как появляется. Имитируя вызов какого-либо метода в классе, требует того, чтобы класс, содержащий метод, был абстрактным либо наследовался от интерфейса. Только тогда мы можем использовать Mock, Stub.
Если мы изменяем код, чтобы было удобнее запускать тесты — это, имхо, уже TDD, пускай и не каноническое.

А в языках с «утиной» типизацией Mock и Stub можно использовать и без сложного наследования ;)
Если честно, я не в курсе, что такое языки с утиной типизацией. Я со своей колокольни смотрю: c#. Какой-то код, кроме как определение интерфейсов, специально для удобства тестов я не вношу. С другой стороны появление интерфейсов не мешает dependency injection.
Если в тестируемом классе вызывается метод какого-то объекта, то на его место мы можем подставить любой другой объект, имеющий такой метод с подходящей для вызова сигнатурой, относятся ли эти объекты к одной иерархии или реализуют какой-то конкретный интерфейс специальной проверки не проводится. «Если что-то выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка». Если у объекта есть требуемый метод, занчит мы можем его вызывать независимо от типа объекта. Более того, зачастую, если метода нет, то мы можем его создать прямо в рантайме :)
Для этого не обязательно практиковать TDD.
Главное не умный тест, а покрытие кода. Т.о., покрыв весь код (методы и операторы чего либо), мы создаем некий контракт. В следующий раз, даже чуть поменяв код, контракт может быть нарушен, и тест нам об этом сообщит. Вот в чем прелесть тестов.
Забыл сказать о еще об одной важной фиче для меня:
5) Возможность дать задачу тим-лидом конкретному исполнителю и при этом иметь возможность быстро вспомнить, что делегировал и правильно ли все пашет?

Правда из п.5 для тим-лида может иметь последствия:
«Так ты же сам сказал как сделать-то надо» ;)
Тестопригодность — это один из важных показателей качества кода. Если нет интерфейса, нет возможности протестировать код и вообще что за аргумент дополнительные интерфейсы, тогда как ты в любой момент времени уверен, что твой код реально работает :-)
Запускаем профайлер производительности, прогоняем какой-нить более-менее стандартный кейс, в 90% случаев видим, что основное время ушло на получение данных из источника данных или их обработку, если вы, конечно, не драйвера пишите или нечто подобное )
Я бы ответил, что надо пробовать и проверять. Без эксперимента или опыта пробовавших людей все равно не понять. Еще встречал очень распространенный контр аргумент Tdd. Что мол все равно всех ошибок этот метод не найдет, к чему морока. Но это же глупо — даже если В два раза удастся сократить уже круто
Про замедление — ерунда, конечно. Точнее сказать, это так называемся экономия на спичках. Если больше заняться нечем, то да, можно и на спичках поэкономить.

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

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

Вот это и есть плохая архитектура! Я заметил, что часто в таких случаях можно этот огромный класс с кучей приватных методов и несколькими вынужденными protected (только для стабирования) разбить на 3-4 мелких и узкоспециализированных классов, каждый из которых занимается ровно своим делом. Они часто легко покрываются тестами (или сразу пишутся с использованием TDD), и там не приходится делать «лишних» или protected-методов для поддержки тестирования. И уже из этих мелких классов, каждый из которых легко и удобно покрыт тестами, как из конструктора, собирается большой класс.

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

Еще возникает вопрос, куда девать всю эту кучу мелких классов, чтобы они не светились и не было соблазна их в коде вызывать. Но обычно это решается просто явным выделением их в некий нижний слой абстракции (хоть в namespace), на который по договоренности из кода нельзя вызывать (вообще, разбиение на слои — это само по себе нетривиальное занятие).

Резюмирую:
— Если есть класс, в котором возникает добавить специальные методы, которые дергаются только из тестов — это антипаттерн.
— Если есть класс с кучей приватных методов, часть из которых хочется сделать protected для тестов — это антипаттерн.
— Разбитие большого сильно инкапсулирующего класса с кучей приватных методов на несколько маленьких классов с минимумом «лишних» методов — это хороший паттерн, но требуется много думать.
— Если сразу писать в стиле TDD, то разбиение на мелкие неизбыточные классы происходит ЗНАЧИТЕЛЬНО проще, чем если тесты писать после кода (или во время рефакторинга), но сам TDD подчас требует больших мыслительных затрат.
Код пишется для людей, незначительно увеличившееся время работы/компиляции окупается легко поддерживаемым кодом, экономией на времени программистов и тестировщиков. В стартапе — хорошим аргументом может быть потребность «встать первым», ибо «тапки одни».
В особо жестоких случаях хороший код потом редуцируется до плохого, но быстрого. Вот только до таких случаев один на миллион.
При использовании ООП появляются накладные расходы на вызов методов (функции-то быстрее). Так что можно было бы предложить оппоненту использовать процедурное программирование.

TDD упрощает код, так как заставляет следовать interface segregation принципу. Но людям, далеким от современного ООП и методологий разработки, это бывает очень сложно объяснить :)
>При использовании ООП появляются накладные расходы на вызов методов (функции-то быстрее)

Не смешивайте парадигму и способ её реализации. На чистом C тоже можно писать в стиле ООП.
Тогда уж лучше писать на Objective-C :) На самом деле первый абзац моего комментария не следует рассматривать слишком серьезно.
Странно что никто не написал очевидных вещей.

1. появляются дополнительные интерфейсы
Если появляется специальной код для юниттестов — это значит, что в консеватории что-то надо менять.
Вы тестируете класс, его апи, даже если он еще не написан, связку вход-выход. Юнит тест стоит сбоку от класса и тестирует класс так, как видят его и могут использовать другие компоненты. И коду тут лишнего появится неоткуда.

Если речь идет о том, что мок можно повесить только на интерфейс, коего класс не имеет, и нужно отдельно создавать и пользовать только в юнит тесте — то это вопрос выбора тула тестирования (можно подумать об «абстракциях», но при нынешнем развитии метапрограммирования и других свистоперделок для уменьшения и ускорения кодирования это представляет собой сложную задачу аргументации). EasyMock и Mockito уже давно это имеют (со сложностями, правда, по статике, но тоже решается PowerMockом).

2. Собственно тоже самое про уровни и усложнение. Кода для юнит теста в тестируемом коде быть не должно, и код юниттестов не используется в самом коде приложения.

Вообще хороший подход такой:
1. Подумали как работает класс, придумалии что у класса будет вот такой апи, в смысле что я подам на этот компонент вот такие данные и получу оттуда только вот такой выход.
У знакомых это называлось Speclet, который отправлялся аналитику и позже вставлялся в шапку класса

2. Пишите юниттест, который реализует этот сценарий проверки.
3. Пишите логику класса, чтобы юнит тест выполнялся.
4. Делаете рефакторинг вашего класса.

Пункты 2-3-4 — это и есть TDD.

Есть резонные другие возражения.
А именно, если ли деньги на такое (это на 100% больше времени в начале, при сноровке — 40%)?
Есть ли острая необходимость иметь такое на всех уровнях вашего приложения? На каких имеет?

Эти вопросы трудны, неоднозначны и ваше мнение могут не разделять люди которые дают бюджет.
В TDD шаги, обычно, поменьше. Интерфейс класса вырисовывается в процессе написания тестов, а не выдумывается заранее. Впрочем, если вы придумали его заранее, модульные тесты в процессе вас обязательно поправят :)
1. Когда у класса две ответственности (что-то вроде ActiveRecord), причем одна ответственность завязана на внешние сервисы (например, хранилище) и инкапсулирована в другую (например, валидация при сохранении), то для того чтобы тесты оставались юнит-тестами, а не интеграционными/функциональными нужно писать лишний код, как минимум разделяя ответственности по методам внутри класса (например, отделяя валидацию от сохранения) и/или вводя одну из вариаций DI (конструируя объект-хранилище, в котором сохраняется наш объект извне его, и передавая в качестве параметра) или вообще вынося одну отвественность в другой класс (Repository например). Приложению в общем-то это не нужно, ему всё равно почему объект не смог сохраниться, главное, что не сохранился, а вот с точки зрения тестирования большая разница не сохранился он из-за невалидных данных или из-за отсутствия коннекта к БД. Во втором случае точно получится не юнит-тест.

2. Если в 2.1 не думать о том, как мы этот API будем тестировать, то в 2.2 вполне вероятно получить не юнит-тест :)
Что пора переходить на BDD, который более красивый для глаза обывателей и проверяет реальный смысл функционала, а не просто полученное значение. Также введение тестирования способствует, собственный опыт, к нахождению лучшей реализации кода, избавлению от ненужного функционала и сокращении времени на ручном тестировании. К дополнительным плюшкам можно добавить спокойные ночи сна, когда наши заграничные заказчики проверяют приложение и не ругаются на ошибки
Можно с тем же успехом применить некоторые практики из BDD, оставаясь при этом на привычном xUnit.
лично я не использовал tdd при разработке, из-за того что оно ужасно скучно

assert_equal value1, value2
assert_equal value3, value4

с одной стороны понимаешь важность, с другой — такая мука писать этот машинный текст. Наверное поэтому я сейчас пишу на ruby/ror + rspec
А вот я никак не могу даже понять отличие BDD от TDD. Сколько не читал и тут, на хабре, и в материалах по рельсам, кроме «более красивый для глаза обывателей» разницу в упор не вижу.
если коротко — в бдд тестируются не внутренности, а то как с Вашим приложением работает пользователь, то есть развитие интеграционных тестов
В TDD внутренности не тестируются :)
В рубийном BDD два цикла тестирования: внешний и внутренний. Внешний — это функциональные тесты на cucumber, внутренний — модульные на rspec. Сначала пишется функциональный тест, который ломается. Затем пишутся модульные тесты, которые заставляют заработать функциональный. Т.е. тот же самый red / green/ refactor но на двух уровнях.

Второе важное отличие — тестирование поведения, а не состояния. Если отбросить сахар, который дает rspec, он особо не отличается от xUnit.

Многие используют только rspec и пишут модульные тесты, без внешнего цикла. Такое BDD не сильно от TDD отличается :)
Абсолютно с вами согласен.

1. Переименовали Assert в Should.
2. Один тестовый метод — один Assert (Should). Это просто именование отдельно каждой проверки.

И все. Все остальное дифирамбы.
На самом деле не совсем так, почитайте мой комментарий выше. Кстати, правило «один тест — один ассерт» появилось в TDD еще до рождения BDD :)
Еще немного и останутся одни дифирамбы :)

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

Перечитал ваши комментарии. Что вы имеете ввиду говоря: тестирование поведения, а не состояния? Возможно изюминка здесь.

На мой взгляд мы всегда тестируем состояние. Мы проверяем конкретные характеристики сущностей. Поведение же — это дельта между исходным состояние (которые мы знаем априори т.к. задаем его руками) и изменившимся (проверяемым в конце теста).
Я стараюсь состояние (переменные класса/объекта) не тестировать, то есть не делать тестов типа:
$obj = SomeClass();
$obj->a = 2;
$obj->b = 2;
$obj->sum();
$res = $obj->c;
assert_equal($res, 4);

а делать что-то вроде
$obj = SomeClass(2,2);
$res = $obj->sum();
assert_equal($res, 4);

При этом состояние объекта меня не интересует особо, например считает он сумму каждый раз при вызове sum() или хранит её где-то в приватных переменных, а то и в БД.
Да даже банальные геттеры/сеттеры приватных переменных я, с некоторых пор тестирую не через рефлексию, а просто вызовом сеттера и геттера. Не знаю, называется это тестированием состояния или поведения, но напрямую состояние я не смотрю.

Правда при копипасте один раз словил ошибку, которую мои тесты не просекли: два сеттера и два геттера писали в одну приватную переменную. тесты типа
$obj->setA(1);
assert($obj->getA(), 1);
$obj->setB(2);
assert($obj->getB(), 2);

проходили, а типа

$obj->setA(1);
$obj->setB(2);
assert($obj->getA(), 1);
assert($obj->getB(), 2);

нет, getA() возвращала 2 :(

Как «феншуйно» (без рефлексии и нескольких ассертов в одном тесте) решить эту проблему ещё не придумал. Или может не надо её решать вообще, а достаточно того, что тест типа $obj->setA(1);
$obj->setB(2);
assert($obj->sum(), 3)

провалится :-/
Попробую я ответить, если вдруг ошибусь, то меня поправят.
Под поведением я понимаю некоторое целенаправленное действие осуществляемое под влиянием внешних и внутренних факторов. Действие это не один конкретный шаг, это возможное некоторое количество шагов. Мы хотим знать что в результате выполнения этих шагов мы не только достигнем результата, но не произойдет нечто такого, что не должно происходить.
На мой взгляд TDD это акцент на проверку корректности одного шага, а вот BDD допускает несколько шагов.

Примеры:
1) «Тестирование открытия файла по пустому имени файла» это небольшой шаг и это ближе к TDD
2) «Тестирование подписи файла по заданному файлу и приватному ключу» — здесь уже больше чем один шаг, но шаги связаны одной единой целью «подписать файл».

Для чего это надо?

Соблюсти баланс между внешними USE-case-ами и мелкими шагами из мира TDD. Если юз-кейсы «птицы высокого полета», а TDD «Насекомое и слишком приземленное», то как нам быть если мы хотим золотую середину?

ЗЫ: Прошу поправить если чего не так, сам вроде как понимаю, но выразить мысль сложно ;)
Я же написал, основе отличие — два цикла тестирования. Посмотрите на картинку (там, правда, названия устаревших инструментов).
image
Два слоя тестов — идея интересная. Надо будет попробовать. Сейчас я обычно строю только внешний слой — что бы излишне не блокировать рефакторинг.

Если я вас правильно понял внешнее кольцо может определяться только классическими тестами (по классификации Фаулера martinfowler.com/articles/mocksArentStubs.html). А внутреннее — как получится?
Внешний слой — это функциональные тесты, так называемые тесты черного ящика. Внутренний слой — это модульные тесты. Модульные тесты обычно пишутся с использованием xUnit, rspec, функциональные — с использованием cucumber, selenium. Проще всего попробовать при реализации какого-нибудь API.
Что касается поведения: мы пишем тест на то, что объект ведет себя так, как мы ожидаем. Например, «два плюс два должно быть четыре». Если тест не проходит, объект не соответствует ожидаемому поведению. Should вместо test просто делает это более очевидным :)
Вот так, с картинкой, понятнее :) Оказывается, я иногда практикую BDD

Правда поскольку английский знаю плохо, то test привычнее, на Should вечно глаз спотыкается и мозг пытается вспомнить что-то там с 4-го класса.
1. Если система правильно спроектирована (а для большинства решаемых на практике задач это сделать не сложно, знание передаётся быстро и в большом объёме зафиксировано в книгах), скорость выполнения не имеет значения. Имеют значение качество сисадмина, вместительность дата-центра и наличие лавэ. Если нет лавэ, проблема не в реализации идеи, а в самой идее.
2. Хорошее ТДД помимо выделения интерфейсов порождает изменение направления зависимостей. Это облегчает создание новых фич в версии номер 2.
3. ТДД облегчает переход к непрерывной интеграции, а та в свою очередь облегчает деплой по кнопке «Пыщь». Оба фактора порождают у разработчиков ответственность поддерживать систему в работоспособном состоянии всё время.
4. ТДД увеличивает объём кода на объём кода тестов, но тесты — это доступная разработчику спецификация. Текст и диаграммы — это недоступная разработчику спецификация.
5. ТДД подталкивает к парному программированию при решении сложных задач, что есть хорошо само по себе, но вдобавок подталкивает к объему владению кодом. Правда, там где применяется парное программирование, необходимо применять санитаров для вылавливания троешников.
6. ТДД упрощает многим хорошистам решение задач, которые умеют решать только отличники. Как правило, в команды приходят хорошисты, если конечно Ваш работодатель не в числе небожителей (яндексы, гугели и прочие фэйсбуки).
Насчёт пункта номер 4. — тут ведб фишка в той самой проблеме, которую поднял автор.
Количество кода увеличивается не только на самих тестах, но и в самом решении поставленной задачи. Код надо писать таким образом, чтоб его можно было проверить — дополнительные интерфейсы и модули…

Однако всё это зачастую относиться к версии «в разработке», так как ТДД хорош тем, что сокращает количество кода, который не попадает в релиз.
НЛО прилетело и опубликовало эту надпись здесь
«При подобном подходе к разработке в коде появляются дополнительные интерфейсы (я практиковал подход к тестированию с помощью Mock'ов, Stub'ов и подмены реализаций интерфейсов) и уровни, усложняющие и замедляющие код»
Он был прав. В той части, которая «дополнительные интерфейсы и уровни, усложняющие код».

Код, ориентированный на модульные тесты, действительно часто содержит лишние уровни абстракции, что усложняет его чтение и понимание. И это _недостаток_ TDD.

Всем желающим написать про «хорошую модульную архитектуру» сразу скажу, что в 90% случаев разрабатываемой системе не нужна подмена какого-то модуля на другой, и разрыв зависимостей вводится только ради TDD (или, что еще хуже, «потому что так правильно»). Свежий пример: есть подсистема создания пользователей, которая создает пользователей в AD и в Exchange. Ее код тривиален: взять логин, передать его в код, отвечающий за AD, а потом в код, отвечающий за Exchange. В системе _никогда_ не будет других модулей (новелла вместо AD, лотуса вместо Exchange), в этом месте никогда не понадобится подмена и, как следствие, лишний уровень абстракции между подсистемой и AD. Но чтобы протестировать подсистему — мы вводим там интерфейс. _Только_ чтобы разорвать зависимость.

Весь этот код — с точки зрения production — чистый оверхед. Он может даже ничего не тормозить (инлайнеры — хорошая штука), но он никому не нужен. И он затрудняет чтение кода — потому что ты вынужден иметь дело с абстракцией даже там, где она не нужна (и закладываться на ошибки ее реализации).

Так что в этом ваш CTO прав.

Но.

Модульные тесты, в обмен на эти недостатки, дают нам какие-то преимущества — уверенность в коде, как следствие — готовность к CI, а потом и CD, ну и так далее. И причем чтобы получить эти преимущества, надо внедрять тестирование планомерно, а не только в избранных кусках, иначе они будут незаметны.

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

(ну еще SRP, в принципе)
НЛО прилетело и опубликовало эту надпись здесь
С SRP проблема в том, что далеко не всегда можно уверенно сказать, где проходят границы responsibility.

(не говоря уже о том, что SRP относится все-таки к code unit, а не обязательно к классу, а метод — вполне себе code unit)
НЛО прилетело и опубликовало эту надпись здесь
… не имеющая гарантированного однозначного решения. Поэтому и дробление функционала на модули не обязательно может быть сделано только одним способом.

Учитывая, что на это дробление могут оказывать влияние и другие факторы, однозначно сказать «вот тут надо писать так, потому что SRP» можно далеко не всегда.

Вообще, code design — это почти всегда выбор баланса между чем-то и чем-то, а не однозначный win-win.
НЛО прилетело и опубликовало эту надпись здесь
И вы, конечно же, всегда можете с уверенностью сказать, какая перед вами категория (и ваше мнение по этому поводу, конечно же, единственно верное).

Завидую вам, вы явно человек, которому не приходилось принимать real-world design decisions.
НЛО прилетело и опубликовало эту надпись здесь
«С каким бы адептом говнокодинга я не говорил, разговор всегда сводится к тому, что «принципы чистого кода это всё красиво, но в реальности без говнокода никак, уж я то знаю, а ты просто не видел больших проектов».»
Удивительно, как легко вы понимаете слова на свой лад. Писать без говнокода можно (если, конечно, ты не работаешь с legacy-системой). Я говорю о другом — не стоит сразу относить код, который, как вам кажется, нарушает какой-то принцип, к говнокоду.

«Но, тем не менее, чтобы построить гибкую, быстро расширяемую систему, нужно пройти этот путь обучения.»
А, понятно. Знаете, я сторонник подхода МакКоннела — don't design for the future. В частности, я не пишу «гибких, расширяемых» систем там, где никогда не понадобится ни гибкость, ни расширяемость.

В частности, тот же МакКоннел явно видел достаточно больших проектов. И он как раз адепт right tool for right problem. Или Казман (в Architecture Evaluation).
НЛО прилетело и опубликовало эту надпись здесь
«Эти принципы достаточно просты и не двусмысленны.»
Например, основной принцип архитектурных решений: it depends.

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

Никогда не обращали внимания, что часть принципов противоречива?
НЛО прилетело и опубликовало эту надпись здесь
«Основной принцип инженерного дела это разделение ответственностей: bit.ly/scOZZG, в программировании это SRP.»
Один из основных, а не основной.

«YAGNI говорит именно о новой функциональности, а не о разделении существующей по модулям, не путайте.»
Разделение существующей функциональности на модули — это уже рефакторинг.

А вопрос «как поделить» часто встает в начале разработки, а не в конце, и вот тут желание «поделить как можно мельче» сильно борется с «dense coding». Баланс между этим неоднозначен и в каждом случае свой. Есть какие-то хорошо понятные вещи (например, DAL надо отделять от бизнеса, а тот — от UI), а есть неочевидные (надо ли изолировать код создания пользователя в exchange от powershell, через который это делается).

Так что it depends. Всегда.
НЛО прилетело и опубликовало эту надпись здесь
«Чтобы легче было ответить на вопрос, задайте другой вопрос: при изменении скольки деталей архитектуры придётся менять этот класс? Для хорошо отделённого класса ответом будет «1».»
(а) у меня есть классы, для которых этот ответ «0»
(б) почему класс, а не code unit?
НЛО прилетело и опубликовало эту надпись здесь
Я спорю ради того, чтобы вы поняли: далеко не всегда в дизайн-решениях есть только один правильный способ.

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

Я пытаюсь до вас донести простую мысль: не всегда то, что вам кажется нарушением SOLID, им является.

(не говоря уже о том, что SOLID — это принципы ООП, а не весь код пишется в ООП)
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Я считаю, Unit Tests — напрасная трата времени (не согласны? спросите мнение разработчика, которого заставят их писать). При изменении/рефакторинге приходится также делать двойную работу, менять еще и тесты. Существующие системы тестирования уродливы и неудобны, требуют много писанины. Писать тесты под каждый класс — очень много мороки, если классов много.

Вдвойне неверно — когда специально меняют код под тесты (чтобы он был совместимым) — нафиг такие тесты, неверно тратить время на всякие моки.

С другой стороны, интеграционные (так это называется?) тесты — вещь неплохая, когда тестируется работа всего приложения, типа если нажать эту кнопку. должно появиться такое-то окно — но опять же, расскажите мне про хотя бы одну *удобную* систему для создания и запуска таких тестов, где тесты можно записывать с действий тестера, а не программировать.
«Я считаю, Unit Tests — напрасная трата времени»
Расскажите нам, как делать любой ci/cd-процесс без unit-тестов.

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

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

«С другой стороны, интеграционные (так это называется?) тесты — вещь неплохая, когда тестируется работа всего приложения, типа если нажать эту кнопку.»
Это не так называется. Это называется «функциональное тестирование».
Вообще само TDD подразумевает стратегию Test First и если разработчик в процессе написания теста воссоздает необходимый функционал, то он сможет увидеть, какие зависимости или ответственности ему следует отделить, вынести за пределы. Да TDD заставляет дробить объект первоначальной задумки, но в конце концов программист, да и качество продукта, от этого только выигрывает. Этот процесс называется рефакторингом. Который постоянно преследует программиста в процессе воссоздания окончательного варианта модуля. Красная полоса -> рефакторинг -> зеленая полоса. Появляется такая великолепная возможность, как повторное использование. И она достигается, например за счет единичной ответственности класса за какой либо функционал. В конце концов применение DI\IoC контейнеров, которые избавляют разработчиков от надобности плодить фабрики. TDD заставляет по другому посмотреть на тестируемый класс или функционал. Тест дает уверенность программисту в том, что он ничего не поломал, при условии что тест не был подогнан под поведение тестируемого модуля.
К сожалению, заказчик не понимает что такое тесты и зачем они нужны. Он платит за работающий продукт. Тесты нужны прежде всего разработчику. На начальном этапе, тесты тормозят разработку, ведь приходится покрывать функционал на каждом из слоев приложения. Написание теста также избавляет программиста от рутины запуска приложения (если каждый запуск может отбирать часть драгоценного времени, которое могло бы пригодиться для выявления проблем) для тестирования его функционала. Позволяет раздробить точки тестирования приложения и сосредоточиться на определенном функционале, что делает тестирование более эффективным. В любом случае использование TDD в купе с применением принципов ООП, делает продукт успешным, а код совершенным!
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории