#4: Прагматичные тесты: TDD, техдолг
Новая рубрика подкаста: разбор тем книги «Программист-прагматик» — топики It's Your Life, The Cat Ate My Source Code и Software Entropy, про ответственность, отговорки и теорию разбитых окон в коде. Во второй части Александр Пахомов признаётся, что пишет тесты до реализации: как устроен цикл TDD, почему он работает даже в легаси-системах, какие плюсы даёт — от тестов-спецификаций до крепкого сна — и лайфхак с красным тестом как чекпоинтом для выхода из состояния потока.
Главное
- It's Your Life: у программистов редкая свобода выбора; как говорил Мартин Фаулер — «вы можете изменить свою организацию или сменить организацию». Сначала попытайтесь улучшить, и только потом уходите.
- The Cat Ate My Source Code: доверие в команде строится на ответственности за свою работу — предоставляйте решения, а не отговорки.
- Software Entropy: техдолг распространяется по проекту как разбитые окна по району — одна допущенная небрежность легитимизирует следующие. Не живите с разбитыми окнами: чините сразу или прикрывайте тикетом.
- Цикл TDD: красный тест → минимально достаточная реализация → зелёный тест → следующий тест; повторять, пока не покрыты все значимые кейсы.
- TDD работает и в легаси: не рефакторьте всё подряд — выносите новую логику в отдельную сущность, разрабатывайте её через тесты и интегрируйте уже проверенной.
- Тесты, написанные до кода, читаются как спецификация, а тестовая инфраструктура и DSL появляются сами: тестом вы формулируете требование, а мысль плохим кодом не выразишь.
- TDD улучшает дизайн продакшн-кода: компоненты становятся инкапсулированными «чёрными ящиками», код — более компонуемым и гибким, а уверенность в нём даёт спокойный сон.
- Лайфхак: красный тест — чекпоинт для выхода из потока; на следующий день запускаешь упавший тест и мгновенно возвращаешься в контекст.
Ссылки
Расшифровка
[00:20] Здарова! Меня зовут Саша Пахомов, и я инженер, который любит своё дело. Это четвёртый выпуск подкаста «Тысяча фичей». А вы знали, что по статистике большинство подкастов не переживают порог в три выпуска? Авторы начинают на энтузиазме, к третьему выпуску он утихает — и новые выпуски перестают выходить. Кажется, это не про меня, и, кажется, мне нравится делать этот подкаст. Так что подписывайтесь на «Тысячу фичей» в Apple Podcasts или на любой другой площадке, чтобы не пропустить новые выпуски. Сегодня вас ждёт новый формат в первой части — разбор глав книги. Потом немного поговорим про тесты — это моя любимая тема. Ну а в конце — суперцитата. Поехали!
[01:11] Есть ли у вас книга, которая навсегда изменила вас — в профессиональном плане, я имею в виду? У меня есть: это «Программист-прагматик». Она повлияла на меня настолько, что даже этот подкаст задумывался по формату как книга «Программист-прагматик» — только в моём переосмыслении. О чём я: если вы заметили, в первых частях выпусков я всегда делюсь практическими советами и навыками, которые использую на постоянной основе, — тот же селф-ревью из прошлого выпуска. Так вот, «Программист-прагматик» — это набор таких практик и советов: их там десятки, и каждый заслуживает отдельного внимания. В 2019 году у книги вышло второе издание, и я решил перечитать её заново. А раз есть подкаст и рубрика с советами и практиками — почему бы не сделать разбор глав книги её частью? Так что знакомьтесь: теперь в первой части выпуска будет разбор одного-двух-трёх топиков из «Программиста-прагматика», второе издание. Сама книга выглядит как набор топиков — тем для обсуждения; вас вводят в контекст, а потом дают совет: как следует поступать, как не следует, какие за этим ценности. Само словосочетание «программист-прагматик» я объяснять сейчас не хочу — это то же самое, что объяснять, кто такой 10x-инженер: каждый интерпретирует по-своему. Думаю, когда мы со временем разберём все главы и советы, у вас сформируется собственное понимание, кто такой программист-прагматик. Отношу ли я себя к ним? Да — хотя, конечно, не всегда получается таковым быть. На этом хватит — начнём разбирать топики.
[03:27] Топик 1 называется It’s Your Life — «это твоя жизнь». Что имеется в виду? Многие программисты жалуются на работу: мало платят, технологии не те, язык устаревший, команда токсичная, хочу релокейт и удалёнку. Слышать подобное достаточно странно. У нас хорошие зарплаты, мы можем работать удалённо, выбирать проекты и делать, что хотим. Поставьте себя на место, не знаю, врачей или учителей — и подумайте, насколько им сложно уехать на Бали и работать оттуда. Практически невозможно. А у программистов такая возможность есть. Так что говорить, что что-то не так с окружением, коллегами или работой, — странно. Совет первого топика звучит так: измените это. Если вам что-то не нравится — поменяйте. Как говорил Мартин Фаулер: вы можете изменить вашу организацию — или сменить вашу организацию. Тут ничего и не добавишь. Не нравится фреймворк — поищите альтернативы в свободное время, пока собирается проект или идёт скучный митинг: погуглите, оцените бенефиты перехода, скажем, с первого Spring Boot на второй или с восьмой Java на семнадцатую, принесите их менеджеру или тимлиду — «давайте возьмём в бэклог, вот так будет лучше». Предпримите попытки улучшить то, что не нравится. Не нравятся токсичные коллеги — поговорите с ними, покажите, что вам не нравится такое отношение. Сначала всегда стоит попробовать изменить вашу организацию — ваш environment. Но если вы сделали попытку, две, три — и получили отказ: «какая, нафиг, одиннадцатая Java, сидим на восьмой, а то и на шестой», «иди отсюда со своим вторым Spring Boot, там рефакторить полгода», коллеги послали к чёрту, — всегда есть вторая опция: сменить организацию. Даже сейчас, в тяжёлый момент, когда рынок ужался, мы всё ещё можем поменять работу. Не стоит об этом забывать.
[06:10] Топик 2: The Cat Ate My Source Code — «кошка съела сорцы». О чём это? Одна из основных идей программиста-прагматика — взятие ответственности: за себя, свои действия, своё обучение, свой проект, свою ежедневную работу. Доверие к тиммейтам и доверие тиммейтов должно строиться на ответственности за свою работу. Пример. Допустим, вы — команда ниндзя, которая выходит на спецзадание ровно в полночь, и вы договорились взять лучшее оружие, которое у вас есть. Тиммейты пришли в назначенное время и место с пушками и автоматами. Вы обещали принести лазер, которым прожжёте дверь, чтобы взломать банк. И вот вы приходите — а лазера при вас нет: «Извините, пожалуйста, у меня кошка любит играть с лазером, она сейчас с ним играет, я не смог его забрать». Представьте, как себя чувствуют остальные ниндзя. Какая им разница, что ваша кошка любит играть с лазером? Или что она его сломала — или написала на него? Их не интересуют отговорки — их интересует, что мы будем делать сейчас. Если кошка занята лазером — возьмите что-то другое, придите с решением. Об этом и совет топика: предоставляйте решения, а не отговорки. Если у вас не компилируется проект и вы не знаете, что делать, — не говорите, что Java плохая, что переход с восьмёрки на одиннадцать невозможен, потому что OpenJDK разрабатывают недалёкие люди. Придумайте, как решить проблему — вплоть до pull request в OpenJDK, у вас есть такая возможность. Не нужно перекладывать ответственность на коллег, фреймворк или язык. Они могут внести свой вклад в ситуацию, это правда, — но предоставлять решение всё равно вам.
[08:35] И последний на сегодня топик 3 — Software Entropy. Я читаю книгу в оригинале, и там используется термин software rot — «программа прогнила», «программа с гнильцой». Мы таких слов не используем, и адекватного перевода у меня нет. Хотя придумывать альтернативы и не обязательно: есть всем известный термин — технический долг. Топик Software Entropy именно про то, как не допустить распространения техдолга на весь проект — и почему его наличие это не круто. Про техдолг как таковой есть куча докладов и мыслей, не хочу сейчас углубляться — возможно, поговорим в других выпусках. А в этом топике суть такая: культура разработки решает. И раскрывается она через метафору — я не очень люблю метафоры, но авторы «Программиста-прагматика» используют их постоянно, так что придётся. Метафора — правило разбитых окон. Представьте улицу в центре Москвы, Москва-сити: всё красивое, стеклянное, вылизанное. А теперь — улицу, где у домов разбиты окна, стены разрисованы граффити, мусор, маргиналы. Как не допустить превращения вашего проекта в улицу с разбитыми окнами и оставить его Москва-сити? Суть в следующем. Как только вы допускаете первую недоработку — у вас Spring Boot с dependency injection, и тут вдруг вы инициализируете статическую глобальную переменную вместо DI — вы вносите техдолг: разбиваете окно. Ваша улица выглядела прекрасно, все окна блестели — и тут вы своим статическим полем разбили окно. Что в этом плохого? Теория разбитых окон говорит: как только на улице разбивается первое окно, в течение нескольких часов или дней (там даже есть ссылки на исследования) в этом районе начинают разбиваться другие окна, дома разрисовывают граффити — и в итоге район становится непригодным для жизни. В проекте то же самое: внесли недоработку — и коллеги (или вы сами) продолжат разбивать окна. Представьте: вы работаете над кодом рядом с той самой статической переменной, и вам нужен такой же синглтон для другой части проблемы. Вы добавите второе статическое поле рядом — «оно же уже есть, сделаю так же; автор сделал статическое поле — видимо, знает, что делает». Так техдолг разрастается на весь проект, и проект превращается в мусорку. Совет топика: не живите с разбитыми окнами. Справедливости ради, иногда недоработки оставлять приходится — есть дедлайны. Тогда прикрываем это тикетом, тудушкой, кидаем exception «not implemented» — любой ценой пытаемся предотвратить распространение плохой практики по кодовой базе. Вместо разбитого окна можно представлять себе вирус, который распространяется, если не использовать правильные механизмы защиты.
[13:27] Ну а теперь ко второй части. Хочу вам признаться: я пишу тесты перед реализацией. Да, вторая часть подкаста будет про TDD. Немного терминологии: TDD — test-driven development, разработка через тестирование; буквально — «разработка, ведомая тестами». Другими словами, сначала пишем тест, потом реализацию.
[13:54] Для начала — как я к жизни такой пришёл? Есть небольшая история. Когда-то я работал в небольшом стартапе, где мы писали платформу для ETL-задач — extract, transform, load. Платформа стояла у заказчика в банке, и каждый день на ней запускались скрипты переливки одних «очень важных» данных в другие очень важные данные — короче, из Oracle данные переливались в Hadoop. Происходило это, как и все ETL-задачи, с периодичностью — каждый день по утрам, по моему времени часов в шесть-семь. Частенько ETL-задачи на нашей платформе крашились, и заказчик, естественно, трубил тревогу: «алерт, всё сломалось, у нас продакшн упал, почините, пожалуйста». И частенько я был тем самым человеком — я был там джуном, максимум мидлом, — который вставал в шесть-семь утра, смотрел, что не так, пытался помочь заказчику, заводил баги и потом чинил их в платформе. Опыт не из приятных. Команда была маленькая, тестировщиков не было, код мы тестировали сами. И вот тогда я понял: наличие тестов — это то, что позволяет мне спать ночами и не вставать в шесть утра от того, что у заказчика опять что-то упало. На своём личном примере я понял, насколько важно писать качественные тесты, в которых ты уверен.
[15:44] Почему это работает — и почему именно TDD, а не просто написание тестов? Сначала объясню, как TDD выглядит на практике. Мы пишем тест — и он падает: даже если реализации ещё нет, мы делаем пустую реализацию интерфейса и формулируем тестом ожидание — «дан такой файл, я ожидаю такой output». Тест, естественно, красный, потому что реализация пустая. Это итерация номер один: мы написали тест, который упал. Затем идём в реализацию и пишем минимально достаточный код, чтобы решить задачу. Если мы читаем файл и должны вернуть его содержимое — в первой итерации мы даже не идём в файл, а просто возвращаем строку, чтобы сделать тест зелёным. Всё, что мы решаем на этом этапе, — перейти из красного состояния теста в зелёное. Круг итерации закончился. Пишем второй тест: первый — зелёный, а новый говорит «если в файле пустая строка — верни то-то» и падает. Идём в реализацию и понимаем: писать if и возвращать ещё одну строку — уже не вариант, по ходу действительно надо идти в файл. Объявляем конструктор, принимающий путь до файла, идём в файловую систему, читаем содержимое — тест работает. Третий тест: если файла не существует, должен быть брошен FileNotFoundException с таким-то месседжем. Запускаем — падает, потому что код отсутствия файла не учитывал: мы решали ровно ту задачу, что была в предыдущем тесте. Теперь переводим из красного в зелёное новый тест, не сломав предыдущие два: пишем проверку существования файла. И такие итерации мы проводим до момента, пока не будем уверены, что протестировали все значимые кейсы, а код в имплементации нас удовлетворяет. Вот краткое описание того, как работает test-driven development.
[18:30] Теперь — почему это работает в реальных проектах. Ведь все (и я только что) рассказывают про TDD на искусственных примерах: какой-то файл читаем, какой-то результат возвращаем. А в реальной жизни задачи в разы сложнее: есть существующая система, в которой до тебя, вероятно, никто тестов вначале не писал, и ничего не готово к тому, чтобы ты так делал. Согласен. Но для меня это работает. Я сейчас разрабатываю достаточно большие системы, в которых есть легаси, — и у меня получается писать тесты вначале. Естественно, я не рефакторю всё на свете, чтобы код стал декомпозируемым и тестируемым. Я делаю иначе: если мне нужно реализовать фичу, я стараюсь сделать её «сбоку» — не вкорячивать ещё один if и ещё один case в нетестируемый класс, оставляя его нетестируемым (ну какое там TDD), а вынести логику, которую я реализую, в отдельную сущность, на ней писать в TDD-стиле, а потом интегрировать её в существующий код. Тогда свою часть кода я, по крайней мере, протестировал — я знаю, что она работает, и могу её интегрировать. Естественно, это не всегда так работает, и, безусловно, не 100% моего кода написано тестами вперёд. Но как только такая возможность есть и я могу сделать это без сильной боли — я делаю.
[20:17] Окей, как писать в этом стиле — поняли. Теперь рассмотрим несомненные плюсы практики TDD. Первый: код тестов становится понятным — тесты выглядят как спецификация. Они не выглядят как «что-то, что что-то тестирует, какие-то ассерты»: их можно прочитать и понять, что они проверяют. Потому что, когда вы писали тест вначале, вы хотели выразить им мысль — «когда у меня нет файла на диске и я вызываю свой код, я получаю FileNotFoundException с таким-то сообщением». Вы формулировали требования к своему коду — поэтому тест читается. По-другому быть не может: нельзя писать в TDD-стиле и иметь отвратительные тесты. Следующий плюс: появляется тестовая инфраструктура, тестовый DSL — domain-specific language. Когда вы пишете тесты вначале и формулируете ими мысль, вы хотите, чтобы они выглядели понятно. Если вы разрабатываете базу данных или систему, которую надо стартовать перед тестами, вы не будете в каждом тесте писать десять строчек запуска — вы напишете тестовую обёртку, которая стартует систему за вас. Так появляется инфраструктура. То же самое с ассертами, предусловиями, вызовами методов: вся эта машинерия будет прятаться за абстракции — просто потому, что мысль плохим кодом выразить очень сложно. Инфраструктура будет появляться, код тестов — становиться чище. А значит, коллегам, которые будут дорабатывать ваш код (а кто-то обязательно будет), будет легче и, возможно, даже приятнее писать следующие тесты, дополнять ваши и рефакторить код, понимая, что он покрыт тестами.
[22:32] Следующий плюс — возрастает уверенность в работоспособности кода. Когда вы маленькими итерациями тестируете разные части системы — есть файл и контент, есть пустой файл, есть отсутствующий файл и exception, — все ваши предположения о том, как должна вести себя система, подкрепляются тестами. И уверенность, что код работает так, как вы хотели, — и будет так работать на продакшне, — несомненно, возрастает. И это не только про тесты: сам продакшн-код становится более гибким и компонуемым — неизбежное последствие написания большого количества тестов, особенно если писать их вначале. В существующих системах вы перестаёте корячить if’ы в существующие if’ы, кейсы и кетчи — вы начинаете выделять сущности и логику. Дизайн системы становится более компонуемым, правильным, чистым — называйте как угодно, но работать с кодом становится легче. Просто потому, что вы начинаете работать с этим кодом — использовать его — ещё до того, как написали: сначала в тесте. Потом, когда ваш код будут использовать в других сервисах или как библиотеку, он уже априори удобнее, компонуемее, конфигурируемее. Когда я пишу в TDD, код вырастает естественно, как растение, — и получается хорошим: как правило, он следует принципу open-closed, потому что, написав тесты вначале, вы хотите потом менять параметры без переписывания. В этом плане TDD прокачивает не только тесты, но и сам код. И ещё замечание: когда вы так пишете тесты, вы начинаете смотреть на систему, компонент, класс как на black box — чёрный ящик, которому можно дать входные данные, как-то его сконфигурировать и получить выход. Именно так выглядит тест — и так начинают выглядеть ваши компоненты: инкапсулированные, принимающие настройки и вход, отдающие результат. Black-box-тестирование тоже двигает нас в правильном направлении с точки зрения дизайна кода.
[24:45] Ну и, наверное, последний плюс, который хочу прямо подчеркнуть: ваш сон становится крепче. Тот самый пример из начала этой части — когда я просыпался рано и были проблемы с настроением и психикой — всё это уходит, как только вы начинаете покрывать код тестами, которые действительно помогают спать спокойно. Я имею в виду не абстрактные тесты, дающие «какое-то покрытие», а именно те, что дают уверенность: код работает так, как вы хотите, — и поэтому, когда он отправился на продакшн, он работает там так, как вы хотели. TDD этому, безусловно, способствует.
[26:38] И лайфхак — продолжу делиться инсайдами, как в первой части, но это уже мой личный лайфхак из практики TDD. Когда мы пишем тест — он красный, чиним — зелёный, пишем следующий — снова красный, и так далее, мы идём по шагам: тест — пять-десять минут, починка — пятнадцать-тридцать (а бывает и тридцать секунд тест, тридцать секунд починка — зависит). В этой итерации на каждом шаге есть зафиксированный результат — чекпоинт: тест либо работает, либо нет. Промежуточно можно коммитить. И из этого процесса легко выйти, когда понимаешь, что сегодня фичу дописать не успеваешь. Знаете проблему отрыва от процесса: когда ты сфокусирован, в состоянии потока, выходить страшно — все мы понимаем, насколько сложно потом обратно войти. Так вот, в TDD выйти суперпросто: написал красный тест и сказал — всё, на этом останавливаюсь, больше ни о чём не думаю, пойду заниматься своими делами и спать. Мы можем себе это позволить, потому что есть чекпоинт, с которого на следующий день легко вкатиться: садишься за стол, запускаешь тест, видишь красный, видишь несходящиеся ассерты — и идёшь чинить элементарную, простейшую багу. Починил — и ты уже в контексте: переходишь к следующему тесту, начинаешь внутренний рефакторинг. Эти промежуточные чекпоинты помогают сохранить контекст, уйти от него и потом к нему вернуться. Пользуйтесь.
[28:39] В заключение, как обычно, полезняшка — и на этот раз это книга «Экстремальное программирование. Разработка через тестирование» Кента Бека. А что ещё могло бы здесь быть, правда? В заголовке этой книги есть цитата, которой я хочу закончить сегодняшний подкаст. Но прежде попрошу вас оставить отзывы в Telegram-канале подкаста «Тысяча фичей» и делиться выпусками с коллегами и друзьями — ведь, как я сказал в начале, похоже, мне нравится делать этот подкаст. А теперь обещанная цитата: «Чистый код, который работает, — вот цель, к которой стоит стремиться». Ну а на этом всё. Услышимся!