Наблюдения за культурой тестирования при разработке через тестирование (TDD)

Оригинал – Observations on the testing culture of Test Driven Development

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

Кент Бек, лидер в области разработки программного обеспечения – автор методологии разработки через тестирование (TDD) в ее современном понимании. Кент также является соавтором фреймворка для тестирования JUint, вместе с Эрихом Гаммой.

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

[KISS, Quality, YAGNI, ...] + [Testing, Specs, ...] == [TDD, ...]

Я глубоко уважаю работу всей жизни Кента не только из-за его блестящих разработок в области программного обеспечения, но и из-за постоянного исследования сути доверия, смелости, обратной связи, простоты и уязвимости. Все атрибуты имели первостепенное значение для изобретения экстремального программирования (XP).

TDD – это принципы и дисциплина, которым следует сообщество XP на протяжении уже девятнадцати лет.

В этой статье я выскажу свое мнение о том, где успела прижиться TDD. Затем мы рассмотрим интригующие личные наблюдения, возникшие при следовании данной методологии. В заключении мы посмотрим почему TDD не получил должного развития. Давайте начнем.

TDD, исследования и профессионализм

Спустя девятнадцать лет TDD все еще обсуждается в сообществе программистов как дисциплина для выбора.

Первый вопрос который задаст любой аналитик: “Сколько или какой процент специалистов в области программного обеспечения используют TDD сегодня?”. Если бы ответ давал друг Роберта Мартина (дядюшка Боб) и друг Кента Бека, то ответ был бы “100%”. Все потому, что дядя Боб считает, что невозможно считаться профессиональным разработчиком, если не практикуешь разработку через тестирование. [1]

Дядюшка Боб занимался несколько лет данной дисциплиной вплотную, поэтому естественно, что в данном обзоре ему будет уделено внимание. Он отстаивал 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 Кент предложил, чтобы тесты определяли архитектуру. Однако, за несколько лет появились истории о том, как спринт-команды натыкались на стену уже через несколько спринтов.

Разумеется, строить архитектуру на основе тестов неразумно. Дядя Боб согласен с другими экспертами в том, что архитектура основанная на тестах – “лошадиное дерьмо”. [1] Требует более обширная карта, но не слишком далеко отстоящая от списков тестов, которые разрабатываются в полевых условиях.

Кент также упоменял об этом спустя несколько лет в книге 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 – Transofrmation Priority Premise). Кажется существует определенный порядок рисков рефакторинга, на который достигается по мере прохождения теста. Выбор верхней трансформации (самой простой) обычно является лучшим вариантом и несет наименьший риск создания тупиковой ситуации.

TPP или, так сказать, Тестовое исчисление дяди Боба – одно из самых интригующих, технлогичных и захватывающих явлений на сегодняшний день. Используйте его как руководство, чтобы код был как можно более простым.

Расчечатайте список TPP и положите его на свой стол. Обращайтесь к нему во время работы, чтобы избегать тупиков. Примите простоту порядка.

На этом все начальные наблюдения заканчиваются. Но прежде чем мы закончим, я хотел бы вернуться к моему первоначальному вопросу, который остался без ответа: “Сколько или какой процент профессионалов в области разработки программного обеспечения используют TDD сегодня?” Мой ответ: “Я думаю, что небольшая группа”. Я хотел бы исследовать это предположение ниже с объяснением причин.

TDD взлетел?

К сожалению, нет. Процент использования, по субъективному предположению низкий, и поиск данных для исследования продолжается. Основываясь на моем опыте найма, руководства командами и самостоятельного опыта разработчика (что меня увлекает) я могу сделать следующие выводы, полагаясь на свои наблюдения.

Причина 1: отсутствие контакта с реальной культурой тестирования

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

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

Тем кто прошел через культуру тестирования, как и я, повезло и столкнуться со всем этим. Мы можем применять этот опыт в новых проектах.

Причина 2: дефицит образовательных ресурсов

Некоторые пытались написать книги на эту тему, например “xUnit Patterns” и “Effective Unit Testing“. Однако, похоже, что не существует четкого определения что и зачем тестировать. В большинстве имеющихся ресурсов нет четкого описания преимущества тестовых утверждений и их проверки.

Проекты с открытым исходным кодом также проходят мимо хороших модульных тестов. В этих незнакомых проектах и первым делом ищу тесты. И почти всегда испытываю разочарование. Я так же могу вспомнить очень немногие случаи восторга, когда тесты не просто присутствуют, но и … читабельны.

Причина 3: университеты не уделяют должного внимания

Мои наблюдения за кандидатами, только что окончившими университет, показывает хорошо известное предположение: никто не обучен дисциплине тестирования. Каждый знакомый мне разработчик впоследствии научился тестирования, некоторые самостоятельно, но большинство из них прошли через культуру тестировании в компании, где она развита.

Причина 4: Требуется сильная увлеченность и стремление заниматься тестами

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

Большая половина просто хочет, чтобы все заработало, достигнув лишь половины того, что сказал Кент Бек: “Сначала заставьте это работать, затем исправьте”. Подчеркну, что заставить все работать – это трудная битва сама по себе.

Делать тестирование качественно – не менее сложно, поэтому давайте в заключении обсудим эту мысль.

Заключение

Формулировка XP от Кента – простое сочетание инстинктов, мыслей и опыта. Эти три уровня являются ступенями к достижению качества исполнения, которое измеряется пороговыми значением. Это отличная модель для объяснения проблемы с TDD.

Порог для чистого выполнения теста высок, поскольку он затмевает высокую планку опыта необходимую для его преодоления. Большинство специалистов никогда ее не достигнут, а те кому это удалось получили опыт неуловимой развитой культуры тестирования.

Из книги XP Explained. Изначально эта схема иллюстрировала качество проектирования, поэтому представьте, что пороговый уровень еще выше.

Создавать и организовывать программное обеспечение достаточно сложно, но тестирование заставляет взглянуть на это совершенно по-новому.

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

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

TDD отчаянно пыталась набрать обороты отчасти из-за того, что кривая обучения тестированию очень крутая. Даже имея опыт и знания ветеранов тестирования, TDD требует уникального и сложного пространства для работы. Однако все должны его попробовать.

Подчеркну следующее. TDD требует всех мыслей, опыта и много другого. Это непросто и требует умения. Я думаю это связано с тем, что он дают разработчикам максимальную производительность непрерывно и неуклонно. Мы все уязвимы в этом процессе, и немного разработчики любят находиться в таком положении.

@Test
fun `Given software, when we build, then we expect tests`() {
    build(software) shoudHave tests
}

Однако TDD – увлекательная дисциплина и инструмент на который можно опереться. Его следует изучить подробно. Во всяком случае, дисциплина способствует лучшему развитию разработчиков, поскольку практика дает преимущества не только отдельным сотрудникам, но и всей команде.

[1] Jim Coplien and Bob Martin Debate TDD