Simon Stålenhag — Tyrannosaurus (http://www.simonstalenhag.se)
“Будьте осторожны с использованием следующего кода — я лишь доказал, что он работает, но я не тестировал его” Дональд Кнут
Техника “Сначала Тест” (Test-First Design, далее TSD) появилась вместе с экстремальным программированием (Extreme Programming, далее XP, кстати, эта абревиатура никак не связана с Windows) и является одним из основных подходов этой методологии. Впервые книжное упоминание этой техники было в Extreme Programming Explained 1999 K.Beck
В основе техники лежит простая идея — перед тем как сделать что-то, напишите на это что-то тест.
“Никогда не пишите код без отказного теста.” Кент Бек
Позднее, техника TFD эволюционировала в технику “Разработки через тестирование” ( Test-Driven Development далее TDD), которая заняла ее место в списке подходов XP.
- Разработка ведется короткими циклами.
- Реализации функции предшествует отказной тест (красно-зеленый рефакторинг).
- Отказной тест только один.
- Реализация функции должна быть максимально простой и достаточной для прохождения теста (тесно связано с принципами KISS и YAGNI).
- После прохождения тестов начинаем безжалостный рефакторинг (на этом этапе мы избавляемся от дублей, тесно связано с принципом DRY)
Я не буду тут пересказывать прекрасный труд Кента Бека, тем кто хочет подробнее разобраться в подходе TDD прямая дорога в магазин за этой книгой (Test Driven Development: By Example 2002 K.Beck):
TDD не работает?
TDD — это динозавр. Но это ни в коем случае не компсогнат, это, скорее, апатозавр. Не меньше. Прошло много лет с момента описания техники. Для информационных технологий это реально большой срок.
Самые прогрессивные деятели мира разработки программного обеспечения признавали и признают до сих пор, что TDD это очень действенный инструмент в арсенале программиста. Это не “серебряная пуля”, но время доказало, что это работает. Мое скромное мнение, что разработка через тестирование — это гениальная техника.
Однако, ситуация беларуского (или какого-нибудь другого похожего) аутсорса по этой части печальна. У нас никогда не было TDD. Никто никогда не воспевал ему оды. Наоборот, я постоянно слышал другое.
В 2010 году я поступил в штат сотрудников отдела QA компании *instinctools. Когда я начал глубже погружаться в автоматизацию, я начал задавать вопросы: “Почему наши разработчики не пишут тесты?” Ответ был, наверное, очевидный — заказчик не платит за это деньги.
Мир, как и беларуский аутсорс, эволюционирует, теперь даже новоиспеченный junior-разработчик или заказчик далекий от программирования знает, что тесты необходимы. Сложно представить себе автопром или пищепром без тестирования (все тогда травились бы едой или разбивались бы на дорогах), хотя, на этом тоже можно было бы сэкономить. Но если к тому, что необходимо писать тесты, все уже давно пришли, то к тому, как именно это делать — еще нет.
Это дорого. Это не купит заказчик. У нас нет на это времени, мы знаем, что это хорошо, но у нас слишком жесткие сроки. Подобную функциональность не получится тестировать этим подходом. Это работает только для модульных тестов. Типичные ответы на вопрос: “Почему не TDD?”.
И при этом, все всеравно пишут тесты, но только делают это в стиле Test-Last.
У нас нет на это времени
Вставая на путь Test-Last разработчик теряет кучу бенефитов TDD.
Время увеличивается. Это факт! Я не знаю почему, но люди думают, что TDD тянет за собой больше времени. Однако, когда Test-Last программист оценивает задачу, у него в голове тут же встает вопрос, а сколько же еще накинуть, чтобы я успел покрыть это все тестами и технической документацией.
Test-Last — это, фактически, шаг назад к каскадной модели. После того, как вы реализуете весь функционал, вы уже не сможете добиться полного покрытия тестами, так как тесты не будут являться сквозными. Ваше тестирование будет поверхностным, я уверен, вы даже будете подстраиваться в тестах под уже написанный код. Попытки протестировать вглубь, будут тянуть за собой время на реализацию заглушек.
Поэтому на тестирование инкапсулированных механизмов вы, скорее всего, махнете рукой.
В противовес этой технике, можно начать использовать TDD. Благодаря TFD и сквозному тестированию, покрытие будет стремиться к 100%. Разрабатывая функцию, вы будете проходить два этапа: проектирование интерфейса и реализацию. На стадии написания теста вам нужно понять, каким будет интерфейс вашей функции, что она будет получать на вход и что она будет возвращать. Каким будет формат входных и выходных данных. Спроектировав интерфейс и написав отказной тест, вы напишите простейшую реализацию, ограничения которой уже заданы в тесте. Это путь к простым и читаемым решениям. Вам останется только описать функцию в документации проекта.
И точно можно сказать, что по началу, вы вряд ли будете выигрывать по времени. Но от функции к функции, от проекта к проекту, ситуация будет меняться. Вы набьете руку и тестирование уже не будет отдельным этапом разработки. Оно как бы сольется с ней. Т.е. это и будет просто разработка. Автоматизированное тестирование не добавляют в оценку отдельным пунктом, оно уже заложено в оценке кодирования.
Убирайте время на отладку и запуск приложения для проверки. Отладка и проверка реализованной функции делается через тесты. К слову, это самый быстрый способ.
Пару месяцев назад, я сел в паре вместе с Димой Сантоцким (наш GUI-разработчик) реализовывать графический компонент для редактирования сущности проекта в разрабатываемой программе. До этого, он не работал в таком стиле. От начала и до конца реализации (это именно логика, там не было верстки) мы переключались только между консолью и IDE. По итогу, Дима заметил, что мы открыли браузер только один раз, в самом конце, для контрольной проверки функционала. Это и вправду волшебство.
Мы не собирали клиентскую часть после каждого изменения, мы разрабатывали именно через тесты.
Со временем и каждодневной практикой диаграмма стоимости примет следующий вид:
Однако, я вижу, что у нас мало кто хочет в это инвестировать. В силу модели аутсорс я сталкивался со многими компаниями и заказчиками, я знаю о чем говорю, и это не о ком-то конкретном. Не принимайте на личный счет. В тоже время, я верю в отдельные бригады программистов, которые активно применяют этот подход.
Стрелять из лука дело совсем не простое. Если его дать вам в руки, едва ли вы тут же попадете в яблочко. Нужны тренировки. Я помню, как я впервые попробовал стрельнуть, стрела улетела куда-то совсем мимо. Инструктор боялся, чтобы я не завалил какого-нибудь рыбака.
Ездить на машине после первого занятия в автошколе тоже вряд ли получится. Тоже самое и с TDD. Из черепашьих гонок, со временем, через тренировки, можно попасть на Формулу-1.
Это работает только для модульных тестов
Integration / System Tests: I/O-First
Хайп 2014-го вокруг “TDD is Dead” привлекателен. Я допускаю, что должны быть компромиссы. Эмулировать видеопоток в тестах крайне сложно (особенно, когда еще нет реализации функции, обрабатывающей этот поток). Или, например, соблюдать подход при тестировании безопасности (security testing). Информационные технологии стремительно развиваются. Дополненная реальность, блокчейн, компьютерное зрение — перед автоматизацией стоит ряд вызовов. Но я вас умоляю, если вы реализовываете простой сервер с CRUD-операциями, то написать интеграционные тесты в стиле TDD не составит труда. Вы знаете какой HTTP-глагол нужен той или иной операции, вы знаете какой статус должен быть в ответе. Продумайте формат HTTP-ответа. И отказной тест с 404 ошибкой готов. А теперь приступайте к реализации конечной точки. Не надо отговорок и усложнений на пустом месте. В случае системных тестов важно определить входные и выходные данные, это самое сложное, далее процесс ничем не отличается от модульных тестов. На тему, есть серия видео встреч от авторов подхода с оппозицией в лице DHH.
E2E: PageObject-First
Приемочные критерии (acceptance criteria) чаще всего принимают форму функциональных или end-to-end тестов. И в этой части, практически все разработчики не понимают, как можно написать тест на страницу, которой еще нет. Перед глазами сразу тонны HTML-разметки и CSS-верстки. Это частая ошибка новичков. Важны функции страницы, элементы управления, контент, а не разметка и верстка. Подобно тому, как дизайнер создает макет страницы, разработчик должен описать страницу шаблоном PageObject на основе дизайна или зарисовки. Это стадия настоящего проектирования. Вы декомпозируете страницу, разбивая ее на элементы и описывая их методами класса. Я бы назвал эту технику PageObject-First, и помните, тест является отказным, нам не нужно знать на этой стадии конкретные способы, с помощью которых мы обнаружим элементы управления, они реализуются позже. В этой части по прежнему можно разрабатывать в стиле TDD. Есть даже устоявшееся название подобной практике: “Разработка основанная на приемочных тестах (Acceptance Test–Driven Development / ATDD)”. Есть и другие вариации (я думаю, что такие аббревиатуры можно клепать каждый день, суть одна):
- “Разработка основанная на тестах заказчика (Customer Test–Driven Development / CDD)”
- “Разработка основанная на тестах пользовательских историй (Story Test–Driven Development / SDD)”
Это дорого
Тесты должны быть на всех фронтах и тылах, но во всем должен быть здравый смысл. Автоматизация это не панацея, ею не стоит увлекаться. Надо понимать, что большую часть тестов, а это модульные, пишут разработчики. Доля тестировщиков-автоматизаторов гораздо меньше. Конечно, все основывается на выделенном бюджете, балом правит заказчик. В этом случае всегда лучше руководствоваться пирамидой тестирования.
Как еще можно сэкономить? Давайте урежем время на документирование, но ценой чего? А почему бы не сделать тесты документацией?
Новое (на самом деле не такое уж и новое) расширение подхода TDD решает эту проблему. Вобрав в себя лучшие принципы Domain-Driven Design Эрика Эванса, TDD эволюционировал в “Разработку основанную на поведении или спецификации (Behavior-Driven Development / BDD или Specification-Driven Development / SpecDD)“
Дэн Норс впервые описал этот подход в 2006 году, презентовав его позже на конференции 2009-го года в Лондоне. Он же создал и один из первых фреймворков на основе этого подхода. Идея в названии. А давайте превратим наши тесты в спецификацию. Т.е. теперь, когда мы пишем отказной тест, мы делаем это через DSL (Предметно-ориентированный язык / Domain-Specific Language), попутно описывая поведение разрабатываемой функции на языке доступном всем.
Начало этой прекрасной идее было положено еще аж в 1987 году в языке Perl в виде Test Anything Protocol (TAP). Одна из первых реализаций человеко-читаемых результатов выполнения тестов, по сути, отчетов.
Подход BDD превратил тесты и в документацию, и в человеко-читаемый и легко понятный отчет для менеджера или заказчика. Это идея проста и не менее гениальна, чем TDD.
Test-Last
Так а что же Test-Last? Это история поросшая мхом? Ненужный мусор, который надо выбросить?
Я так не считаю. Для меня это неотъемлемая часть TDD. Звучит странно, не правда ли? Да, именно так. Я считаю Test-Last нулевым шагом в TDD. Именно с этой техники я советую всем начинать. Ведь все познается в сравнении. Только те, кто работал в этом стиле, а позже перешел на TDD, могут осознать всю ценность идей Кента Бека. Юному джедаю нужно побывать на обеих сторонах. Если вы сейчас работаете в стиле Test-Last, то продолжайте. Я серьезно. Старайтесь добиться как можно более полного покрытия, изолируйте тесты, пишите заглушки и пытайтесь дотянуться до самых спрятанных функций. При оценке и планировании, прикидывайте на глаз, сколько займет времени реализовать тесты.
Новый же проект, попробуйте начать разрабатывать в стиле TDD.
***
Если у вас есть какие-то замечания или дополнения, я буду рад их увидеть в комментариях или пишите на artur.basak.devingrodno@gmail.com
Автор: Artur Basak