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

Кент Бек, лидер в области разработки программного обеспечения, также является современным изобретателем разработки через тестирование (TDD). Кент также был соавтором JUnit, широко используемой среды тестирования, вместе с Эрихом Гаммой.

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

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

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

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

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

TDD, учеба и профессионализм

Девятнадцать лет в TDD все еще обсуждаются как дисциплина в сообществе разработчиков.

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

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

Однако никто не задает дополнительный вопрос: Определение практики - это преднамеренное использование - но в нем не указывается количество или процент, верно? Моя субъективная оценка такова, что большинство инженеров-программистов не практиковали TDD в короткие сроки.

В действительности ситуация такова, что мы не знаем, поскольку процент практики широко не изучен. Единственное конкретное измерение, которое у нас есть, - это небольшая коллекция компаний, собранная в WeDoTDD. WeDoTDD отслеживает эти компании. Собеседования проводятся с теми, кто практикует 100% времени, но этот список невелик. Это также неполно, поскольку поиск показывает, что другие более крупные магазины практикуют, но, возможно, не на полную мощность.

Если мы не знаем, сколько из них практикуют, следующий вопрос: «Насколько эффективна TDD на основе измеренных выгод?»

На протяжении многих лет проводились исследования, доказавшие эффективность TDD. Записи включают хорошо известные отчеты от Microsoft, IBM, Университета Северной Каролины и Университета Хельсинки.

Эти отчеты доказывают, что плотность дефектов снижается на 40–60% в обмен на увеличение трудозатрат и времени выполнения на 15–35%. Эти цифры также начали отражаться в книгах и в новых отраслях, таких как сообщество DevOps.

Когда на эти вопросы наполовину ответили, последний вопрос: «Чего мне ожидать, когда я начну выполнять TDD?» Вам повезло, потому что я сформулировал свои наблюдения за TDD. Давайте рассмотрим их дальше.

1. Команды TDD, выражающие подход

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

IDE разработчика (интегрированная среда разработчика) превратилась в резиновую уточку, требующую интенсивного разговора. Как минимум, магазины TDD должны гудеть от этого типа преобразования.

Подумайте, а затем расскажите о своих следующих шагах.

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

2. TDD команды мышечной памяти

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

Мышечная память - ключ к хорошему настроению и плавности. TDD требует этого из-за повторения выполнения.

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

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

3. TDD команды, мыслящие, по крайней мере, немного вперед

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

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

Ведите свой список тестов как ястреб. Отмечайте предметы по ходу дела. Никогда не садитесь за руль без него. Подумайте!

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

// A Test List
// "" -> does not validate
// "a" -> does not validate
// "aa" -> validates
// "racecar" -> validates
// "Racecar" -> validates
// print the validation
// have a blueberry ale

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

4. TDD требует общения с другими

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

Вождение без TDD вызовет реализацию ненужной сложности. TDD, выполняемый бездумно, не менее опасен.

Если в тестовом списке есть пробелы, громко говорите.

В TDD разработчик должен понимать, что строить, основываясь на представлении владельца о требованиях и не более. Если необходимость неясна в контексте, список тестов начнет разрушаться. Этот разрыв потребует разговора. Прямые преобразования могут быстро превратиться в доверие и уважение. Теперь короткие петли обратной связи установлены.

5. TDD требует итеративной архитектуры

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

Конечно, неразумно проводить тесты, управляющие всей архитектурой. Дядя Боб согласился с другими экспертами, что архитектура, основанная на тестах, - это «дерьмо для лошадей». [1] Требуется карта большего размера, но не выше распределенных тестовых списков, над которыми работают в полевых условиях.

Кент также упомянул об этом много лет назад в книге TDD By Example. Параллелизм и безопасность - две важные области, в которых TDD не может работать, и разработчик должен заботиться об этом отдельно. Вольно выраженный параллелизм через проектирование системы находится на другом уровне, и над ним нужно работать итеративно и совместно с TDD. Параллелизм стал реальностью сегодня, поскольку некоторые архитектуры стремятся к реактивным расширениям, зениту построения параллелизма.

Создайте большую карту организации. Видение, которое идет немного вперед. Убедитесь, что вы руководите командой одинаково.

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

6. TDD выявляет уязвимость модульных тестов и дегенеративную реализацию

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

Например, нижеприведенное решение решает все тесты на основе гипотетического несовершенного палиндрома, необходимого для бизнеса. Он был разработан с помощью TDD.

// Not an imperfect palindrome.
@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()
}

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

// Too generic of a solve based on tests provided
fun String.validate() = if (isEmpty() || length == 1) false else toLowerCase() == toLowerCase().reversed()
// Is the best implementation and solves all tests
fun String.validate() = length > 1

length > 1 можно назвать дегенеративной реализацией. Реализации достаточно, чтобы решить поставленную задачу, но сам по себе ничего не говорит нам о проблеме, которую мы пытаемся решить.

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

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

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

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

7. TDD выявляет петлю обратной связи о завершении утверждения

Сделайте шаг назад. Что касается следующих двух явлений, мы посетим странные повторения. В первую очередь, давайте взглянем на FizzBuzz. Вот наш список тестов.

// Print numbers 9 to 15. [OK]
// For numbers divisible by 3, print Fizz instead of the number.
// ...

Мы находимся в нескольких шагах. Теперь у нас есть провальный тест.

@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 <?>.

Естественно, если мы дублируем ожидаемые данные утверждения в assertEqualsIt, результат будет достигнут, и тест пройден.

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

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

8. TDD раскрывает предпосылку приоритета трансформации

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

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

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

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

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

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

TDD взлетел?

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

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

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

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

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

Причина 2: нечеткие образовательные ресурсы

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

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

Причина 3: отсутствие внимания в университетах

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

Причина 4: Требуется карьера, полная страсти к испытаниям

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

Большинство из них хотят, чтобы все работало, достигнув лишь половины того, что сказал Кент Бек: «Сначала заставьте это работать, а затем исправьте». Я сочувствую, что заставить вещи работать - это трудная битва сама по себе.

С тестированием сложно справиться хорошо, поэтому давайте остановимся на этой мысли.

Заключение

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

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

Программное обеспечение достаточно сложно создать и организовать. Тестирование выводит его на совершенно новый уровень просвещения.

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

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

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

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

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

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

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

[1] Джим Коплиен и Боб Мартин обсуждают TDD