Это не вводный курс по разработке через тестирование (TDD), а наблюдения по поводу перезагрузки этой дисциплины и проблем модульного тестирования.
Это перевод статьи Observations on the testing culture of Test Driven Development, впервые опубликованной на freeCodeCamp.
Экстремальное программирование
Автор методологии разработки через тестирование (TDD) в ее современном понимании — лидер в области разработки программного обеспечения Кент Бек. Он также — соавтор фреймворка для тестирования JUnit, вместе с Эрихом Гаммой.
В своей книге XP Explained (второе издание) Кент описывает, как принципы формируются на пересечении ценностей и практик. Когда мы строим концепцию и подставляем ее в формулу, то получаем преобразование:
[KISS, Quality, YAGNI, ...] + [Testing, Specs, ...] == [TDD, ...]
Я глубоко уважаю работу всей жизни Кента не только из-за его блестящих разработок в области программного обеспечения, но и из-за постоянного исследования сути доверия, смелости, обратной связи, простоты и уязвимости. Все эти атрибуты имели первостепенное значение для изобретения экстремального программирования (XP).
TDD — это принципы и дисциплина, которым следует сообщество XP на протяжении уже 19 лет.
TDD, исследования и профессионализм
Спустя 19 лет в сообществе программистов все еще нет единого мнения по поводу TDD как дисциплины.
Первый вопрос, который задаст любой аналитик: «Какой процент профессионалов в области программного обеспечения используют TDD сегодня?». Если бы ответ давал Роберт Мартин (дядюшка Боб), друг Кента Бека, то он был бы: «100%». Все потому, что дядя Боб считает, что невозможно считаться профессиональным разработчиком, если ты не практикуешь разработку через тестирование.
Дядюшка Боб несколько лет занимался этой дисциплиной вплотную. Он отстаивал TDD и существенно расширил границы этой дисциплины. Я предельно уважаю дядю Боба и его прагматичный догматизм.
Однако никто не задает вопрос: «Практиковать — означает сознательно использовать, но ведь это не позволяет судить о том, насколько часто?». По моей субъективной оценке, большинство программистов не практиковали TDD даже недолго.
Реальность такова, что мы не знаем цифр, поскольку никто это не исследовал. Единственные конкретные данные есть по небольшой группе компаний на сайте WeDoTDD. Здесь вы найдете статистику по компаниям, практикующим TDD, и интервью с теми кто делает это постоянно, но этот список невелик. Он не может быть полным, поскольку простой поиск показывает, что и более крупные компании используют TDD, но возможно не на полную мощность.
Если мы не знаем сколько компаний его практикуют, то назревает следующий вопрос: «Насколько эффективна TDD с позиции измеримой выгоды от использования?».
На протяжении многих лет проводились исследования, доказывающие эффективность TDD. Отчеты были получены в том числе от Microsoft, IBM, Университета Северной Каролины и Университета Хельсинки.
Отчеты в определенной степени доказывают, что плотность дефектов снижается на 40–60% в обмен на рост усилий, при котором время на выполнение возрастает на 15–35%. Эти цифры уже начали отражаться в книгах и новых отраслевых методологиях, таких как DevOps.
Получив частичные ответы на эти вопросы, переходим к последнему: «Чего мне ждать от TDD?» Вам повезло, потому что ответ на него я сформулировал из личных наблюдений. Давайте их рассмотрим.
1. TDD требует вербализации
Практикуя TDD, мы сталкиваемся с феноменом «обозначения цели». Проще говоря, короткие действия-проекты по созданию неудачных и успешных тестов ставят перед разработчиком интеллектуальный вызов. Разработчик должен четко сказать: «Я считаю, что этот тест будет пройден», или: «Я считаю, что этот тест не будет пройден», или: «Я не уверен, дайте мне подумать после того, как попробую этот подход».
IDE (интегрированная среда разработки) стала для разработчика резиновой уточкой, которая умоляет с ней активно беседовать. Как минимум, в TDD-компаниях разговоры такого плана должны сливаться в сплошной гул.
Подумайте, а затем расскажите о своих следующих шагах.
Подобное подкрепление — ключ в коммуникации: не только для прогнозирования вашего следующего действия, но и для закрепления концепции написания простейшего кода для успешного прохождения модульного теста. Если же разработчик замолкает, он почти наверняка сбился с пути и должен снова его искать.
2. TDD развивает мышечную память
По мере продвижения через первые циклы TDD разработчик будет быстро уставать, поскольку будет работать с неудобным процессом и постоянно буксовать. Это обычная ситуация для любой деятельности, с которой человек только начал взаимодействовать, но еще не освоил. Разработчики будет страраться прибегать к шорткатам для улучшения и оптимизации цикла, чтобы набить руку и улучшить мышечную память.
Мышечная память — ключ к хорошему настроению и плавности. В TDD это необходимо из-за повторения действий.
Распечатайте шпаргалку с шорткатами. Изучите столько горячих клавиш в вашей IDE, сколько необходимо, чтобы ваши циклы были эффективными. Затем продолжайте поиски.
Всего за несколько сеансов разработчик станет экспертом в выборе шорткатов и горячих клавиш, включая сборку и запуск испытательного стенда. Создание новых артефактов, выделение текста и навигация в IDE станут естественными. Вы станете настоящим профессионалом и освоите шорткаты рефакторинга, такие как извлечение, переименование, генерация, подъем, переформатирование и спуск.
3. TDD заставляет хотя бы немного думать наперед
Каждый раз, когда разработчик приступает к TDD, он должен иметь конкретную краткую мысленную карту того, что необходимо решить. В традиционном подходе к написанию кода, это не всегда применимо, поскольку задача может быть на макроуровне или иметь исследовательскую природу. Там разработчик не знает, как решить проблему, но может знать примерную цель. Для достижения этой цели модульные тесты игнорируются.
Начало и завершение работы стоит превратить в ритуал. Сначала подумайте и составьте список того, что надо сделать. Поиграйте с этим. Добавьте еще. Начните делать и подумайте еще раз. Отмечайте выполненное. Повторите несколько раз. Окончательно подумайте и остановитесь.
Следите за своим списком тестов как ястреб. Отслеживайте то, что уже сделано, ставьте галочки. Не сворачивайтесь, пока не появится хотя бы одна. Думайте!
Составление списка может занять некоторое время, и оно не является частью цикла. Однако список необходимо подготовить перед началом цикла. Если у вас его нет, вы не будете знать, куда двигаться. Всегда имейте карту перед началом работы.
// Список тестов // "" -> не проходит // "a" -> не проходит // "aa" -> проходит // "racecar" -> проходит // "Racecar" -> проходит // вывести валидацию // выпить черничного эля
Разработчик должен составить список тестов — так учит Кент Бек. Список тестов позволяет направить решение задачи в ближайшие циклы. Над этим списком тестов всегда нужно работать и обновлять его до начала циклов. После того как список тестов решен, за вычетом последнего шага, цикл останавливается на красном цвете с неудачным тестом.
4. TDD требует общения
Когда список будет заполнен, некоторые шаги могут оказаться заблокированы, поскольку на них не вполне ясно описано, что делать. Разработчик не может разобраться в списке тестов. Или список содержит слишком много предположений и неточных формулировок. Рекомендуется остановиться, если такое произошло.
Действуя без TDD, можно получить избыточно сложные реализации. Работа в стиле TDD, но без списка, опасна апатичной бездумностью.
Если в списке тестов есть пробелы — говорите об этом.
В TDD разработчик должен понимать, что делать, основываясь на представлении заказчика о требованиях и не более. Если требование имеет неясный контекст, список тестов начнет разрушаться. Это потребует обсуждения. Спокойные обсуждения способствуют росту доверия и уважения, а кроме того, помогают установить короткие циклы получения обратной связи.
5. TDD требует итерационной архитектуры
В первом издании книги XP Кент предложил, чтобы тесты определяли архитектуру. Однако, за несколько лет появились истории о том, как спринт-команды натыкались на стену уже через несколько спринтов.
Разумеется, строить архитектуру на основе тестов неразумно. Дядя Боб согласен с другими экспертами в том, что архитектура основанная на тестах — «чушь». Требуется более обширная карта, но не слишком далеко отстоящая от списков тестов, которые разрабатываются в полевых условиях.
Кент упоминал об этом спустя несколько лет в книге TDD by Example. Параллелизм и безопасность — две основные области, в которых TDD не может работать, и разработчик должен заботиться об этом отдельно. Можно сказать, что параллелизм — другой уровень проектирования системы, и над ним нужно работать итеративно и согласовывая с TDD. Это очень актуально сегодня, поскольку некоторые архитектуры стремятся к реактивной парадигме и реактивным расширениям — зениту построения параллелизма.
Создайте большую карту организации и видение, которое немного забегает вперед. При этом убедитесь, что вы с командой в одном темпе.
Однако самая важная идея — организация системы, с которой TDD не может эффективно справиться самостоятельно. Это связано с тем, что модульные тесты являются низкоуровневыми. Итеративная архитектура и оркестрирование TDD сложны на практике и требуют доверия между всеми членами команды, применения парного программирования и тщательного анализа кода. Нет четкого способа, как это сделать, но становится очевидным, что короткие сеансы итеративного проектирования необходимо проводить в унисон с построением списков тестов в предметной области.
6. TDD выявляет уязвимость модульных тестов и дегенеративную реализацию
У модульных тестов есть забавное свойство, и TDD его раскрывает: они не могут доказать правдивость. Э.В. Дейкстра работал над возможностью математических доказательств в нашей профессии, чтобы устранить этот пробел.
Приведем пример, в котором решаются все тесты, связанные с гипотетическим несовершенным палиндромом, продиктованные бизнес-логикой. Пример разработан с помощью TDD.
// Не несовершенный палиндром @Test fun `Given "", then it does not validate`() { "".validate().shouldBeFalse() } @Test fun `Given "a", then it does not validate`() { "a".validate().shouldBeFalse() } @Test fun `Given "aa", then it validates`() { "aa".validate().shouldBeTrue() } @Test fun `Given "abba", then it validates`() { "abba".validate().shouldBeTrue() } @Test fun `Given "racecar", then it validates`() { "racecar".validate().shouldBeTrue() } @Test fun `Given "Racecar", then it validates`() { "Racecar".validate().shouldBeTrue() }
Действительно, в этих тестах есть дыры. Модульные тесты хрупки даже для самых тривиальных задач. Мы никогда не можем доказать свою правоту, потому что это потребовало бы огромного умственного труда, а необходимые затраты были бы невообразимы.
// Слишком обобщенная реализация, сделанная на основе представленных тестов fun String.validate() = if (isEmpty() || length == 1) false else toLowerCase() == toLowerCase().reversed() // Это наилучшая реализация, решающая все тесты fun String.validate() = length > 1
length > 1
можно назвать вырожденной реализацией. Она вполне достаточна для решения поставленной задачи, но сама ничего не сообщает о проблеме, которую мы пытаемся решить.
Вопрос в том, когда разработчик должен перестать писать тесты? Ответ кажется простым. Когда их становится достаточно с точки зрения бизнес-логики, а не по мнению автора кода. Это может повредить нашему энтузиазму творца, а еще нас смущает простота. Но эти чувства уравновешиваются удовлетворением от вида собственного чистого кода и возможностью уверенного рефакторинга. Все выглядит чисто и опрятно.
Имейте ввиду, что модульные тесты подвержены ошибкам, но необходимы. Поймите их силу и слабость. Мутационное тестирование может помочь восполнить их пробелы.
TDD имеет преимущества, но лишает нас возможности строить ненужные песчаные замки. Это ограничение, которое позволяет нам двигаться быстрее, дальше и безопаснее. Возможно именно это имел в виду дядя Боб, описывая, что по его мнению означает «быть профессионалом».
Но! Независимо от того, насколько ненадежными могут показаться модульные тесты, они — ключевая необходимость. Они помогают превратить страх в смелость. Тесты обеспечивают щадящий рефакторинг кода; более того, они могут послужить руководством и документацией любому новому разработчику, который сможет сразу войти в курс дела и начать трудиться на пользу проекта — если этот проект хорошо покрыт модульными тестами.
7. TDD демонстрирует обратный цикл выполнения тестовых утверждений
Сделаем еще один шаг вперед. Что касается следующих двух феноменов, давайте исследуем странные повторяющиеся события. Для начала давайте бегло рассмотрим FizzBuzz. Вот наш список тестов.
// Вывести числа от 9 до 15. [OK] // Для чисел, кратных 3, вывести Fizz вместо числа. // ...
Прошли на несколько шагов вперед. Теперь наш тест проваливается.
@Test fun `Given numbers, replace those divisible by 3 with "Fizz"`() { val machine = FizzBuzz() assertEquals(machine.print(), "?") } class FizzBuzz { fun print(): String { var output = "" for (i in 9..15) { output += if (i % 3 == 0) { "Fizz " } else "${i} " } return output.trim() } } Expected <Fizz 10 11 Fizz 13 14 Fizz>, actual <?>.
Естественно, если мы продублируем ожидаемые данные утверждения в assertEquals
, результат будет достигнут и тест пройден.
Иногда провальные тесты выдают корректный результат, необходимый для прохождения теста. Не знаю, как назвать такие события… может быть, вуду-тестирование.
Ваш опыт может варьироваться в зависимости от лени и подхода к тестированию, но я много раз замечал, что подобные вещи происходят когда человек старается получить реализацию, нормально работающую с готовыми и предсказуемыми наборами данных.
8. TDD демонстрирует условие очередности преобразований
TDD может поймать вас в ловушку. Бывают ситуации, когда разработчика могут запутать преобразования, которые он применяет для реализации. В какой-то момент тестовый код становится узким местом. Возникает тупик. Разработчик должен отступить и обезвредить себя, удалив часть тестов, чтобы выбраться из ямы. Разработчик оказывается незащищенным.
Дядя Боб, вероятно, сталкивался с этими тупиками в своей карьере, а затем он, скорее всего, осознал, что акт прохождения теста должен требовать определенного порядка, чтобы снизить риск тупиковой ситуации. В то же время, он должен осознать еще одно условие. Чем конкретнее становятся тесты, тем более общим становится код.
Это и есть условие очередности преобразований (TPP — Transformation Priority Premise). Кажется, существует определенный порядок рисков рефакторинга, который достигается по мере прохождения теста. Выбор верхней трансформации (самой простой) — обычно лучший вариант, который несет наименьший риск создания тупиковой ситуации.
TPP — одно из самых интригующих, технологичных и захватывающих явлений сегодня. Используйте его как руководство, чтобы код был как можно более простым.
Распечатайте список TPP и положите его на свой стол. Обращайтесь к нему во время работы, чтобы избегать тупиков. Примите простоту порядка.
На этом начальные наблюдения заканчиваются. Но я хотел бы вернуться к первоначальному вопросу, который остался без ответа: «Какой процент профессионалов в области разработки программного обеспечения используют TDD сегодня?». Мой ответ: «Думаю, что небольшой».
TDD взлетела?
К сожалению, нет. Процент использования, по субъективному предположению низкий. Основываясь на моем опыте найма, руководства командами и самостоятельного опыта разработчика, я могу сделать следующие выводы о причинах этого.
Причина 1: Отсутствие контакта с реальной культурой тестирования
Мое обоснованное предположение состоит в том, что у большинства разработчиков программного обеспечения не было опыта обучения и работы в рамках культуры тестирования.
Культура тестирования — это место, где разработчики сознательно практикуются и совершенствуются в искусстве тестирования. Они постоянно обучают тех, кто не обладает квалификацией в этой области. Каждая пара и каждый запрос на включение — это цикл обратной связи, помогающий развивать у людей навыки тестирования. Кроме того существует серьезная поддержка и чувство локтя в рамках всей инженерной цепочки. Все менеджеры понимают и верят в тестирование. Когда сроки становятся жесткими, дисциплина тестирования не послабляется — она сохраняется.
Тем, кто прошел через культуру тестирования, как и я, повезло столкнуться со всем этим. Мы можем применять этот опыт в новых проектах.
Причина 2: Дефицит образовательных ресурсов
Некоторые пытались написать книги на эту тему, например xUnit Patterns и Effective Unit Testing. Однако похоже, что не существует четкого определения, что и зачем тестировать. В большинстве имеющихся ресурсов нет четкого описания преимущества тестовых утверждений и их проверки.
Проекты с открытым исходным кодом также проходят мимо хороших модульных тестов. В этих незнакомых проектах и первым делом ищу тесты. И почти всегда испытываю разочарование. Я так же могу вспомнить очень немногие случаи восторга, когда тесты не просто присутствуют, но и читабельны.
Причина 3: Университеты не уделяют должного внимания
Мои наблюдения за кандидатами, только что окончившими университет, показывает хорошо известное предположение: никто не обучен дисциплине тестирования. Каждый знакомый мне разработчик впоследствии научился тестированию, некоторые самостоятельно, но большинство из них — через культуру тестировании в компании, где она развита.
Причина 4: Требуется сильная увлеченность и стремление заниматься тестами
Нужна страсть заниматься тестированием, чтобы интересоваться им и понимать детали и преимущества на длинном отрезке времени. Вы должны быть жадными и зацикленными на чистом коде и улучшении своего мастерства в его написании.
Большая половина просто хочет, чтобы все заработало, достигнув лишь половины того, что сказал Кент Бек: «Сначала заставьте это работать, затем исправьте». Подчеркну, что заставить все работать — это трудная битва сама по себе.
Делать тестирование качественно — не менее сложно, поэтому давайте в заключении обсудим эту мысль.
Заключение
Формулировка XP от Кента — простое сочетание инстинктов, мыслей и опыта. Эти три уровня — ступени к достижению качества исполнения, которое измеряется пороговыми значением. Это отличная модель для объяснения проблемы с TDD.
Порог для чистого выполнения теста высок, поскольку затмевает высокую планку опыта, необходимую для его преодоления. Большинство специалистов никогда ее не достигнут, а те, кому это удалось, получили опыт неуловимой развитой культуры тестирования.
Создавать и организовывать программное обеспечение достаточно сложно, но тестирование заставляет взглянуть на это совершенно по-новому.
Изначально у меня было чувство, что тестирование важно, но опыт культуры тестирования пришел позже. На это у меня ушли годы размышлений в течении всей карьеры, но без опыта работы в развитой тестовой культуре я бы не поднялся выше порогового уровня.
Я уверен, что многие разработчики тоже думают об этом, но не видят истинных преимуществ культуры тестирования из-за отсутствия соответствующего опыта.
TDD с трудом набирало обороты отчасти из-за того, что кривая обучения тестированию очень крутая. Даже с опытом и знанием ветеранов тестирования TDD требует уникального и сложного подхода. Однако все должны его попробовать.
Подчеркну следующее. TDD требует умения мыслить, опыта и многого другого. Это непросто и требует навыков. Но TDD непрерывно и неуклонно выводит разработчиков на максимальную производительность. Мы все уязвимы в этом процессе, немногие разработчики любят находиться в таком положении.
@Test fun `Given software, when we build, then we expect tests`() { build(software) shouldHave tests }
Однако TDD — увлекательная дисциплина и инструмент на который можно опереться. Его следует изучить подробно. TDD способствует развитию разработчиков, поскольку дает преимущества не только отдельным сотрудникам, но и всей команде.
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: