Рефакторинг. Эпизод III. Unit-тесты
«Задумывались ли вы, зачем у машины тормоза? Инженеры изобрели машину, чтобы быстрее перемещать людей. Но зачем они добавили в неё подсистему, которая замедляет перемещение и противоречит цели изобретения? В теории машина может ехать настолько быстро, насколько это позволяет двигатель, особенно на прямой дороге. Но именно благодаря тормозам машина будет ехать быстро: без них вы бы и не рискнули разгоняться на реальных дорогах. Тесты нужны для этого же: чтобы «разогнавшись» вы могли безопасно изменить «траекторию» своего продукта». – Kevlin Henney.
В этом эпизоде мы поговорим о горячей теме unit-тестирования. Из материалов вы узнаете:
- откуда такой разброд и шатание в понимании людей что такое unit-тест,
- что мы хотим от тестов, а что получаем в итоге,
- как писать тесты так, чтобы со временем не захотелось их удалить.
Из материалов предыдущего эпизода мы уже узнали, что unit-тесты обязательны перед началом почти любого рефакторинга. Но что такое тест? Функция, которая вызывает нужный нам метод и проверяет, что мы получили ожидаемый результат? В целом, да. Но не всё так просто.
Если начать наивно писать тесты, вы быстро столкнетесь со следующими сложностями:
- Написание новых тестов начинает потреблять существенный объем времени.
- Написанные вчера тесты мешают вносить изменения сегодня, даже если суть этих тестов при этом не меняется.
- Те тесты, которые вы всё-таки смогли написать, всё равно не ловят ошибок, хотя процент покрытия тестами кода близится к 100%.
«Тесты не работают! От них становится только хуже! Очередная утопическая идея бородатых дедов из прошлого века» – скажет новичек с ещё вчера горевшими глазами.
«С ними действительно не всё так просто по фундаментальным причинам. Но разобравшись, есть шанс получить от них больше профита, чем проблем» – отвечу я ему.
Ранее я уже читал книги, в названии которых упоминались unit-тесты, и по PHP, и по Java. Но все они были скорее по тупой механике – просто отвечали на вопрос: «Я хочу написать тест, как мне этого сделать на этом языке?». Но я хотел разобраться в сути практики unit-тестирования, найти ответы ровно на те вопросы, которые упоминал в начале этой статьи. Как обычно это и бывает, поводом к этому стал очередной профессиональный вызов на работе: мне предстояло написать с нуля расчет развесистого контракта для новой версии виджета “Список товаров корзины” для Backend-driven UI. И заодно не повторить ошибок дизайна кода и тестов прошлой версии виджета, от которых очень болело у всей команды разработчиков. И я начал копать.
«Unit Testing Principles, Practices, and Patterns». Vladimir Khorikov
С докладами и статьями Владимира я был давно знаком, и вопросов к ним у меня было достаточно. Поэтому я долго не воспринимал его книгу всерьёз, да ещё и с таким вызывающим заголовком. Но меня так подпекала проблема, что я решил дать автору ещё один шанс.
Эта книга с самого начала дала такого погружения в проблематику, которого я и не ожидал. Тут и про те самые «школы» unit-тестирования, которые разделили разработчиков на два лагеря: детройтскую и лондонскую. И про свойства тестов, или почему идеальных тестов не существует. Также автор разбирает принципы интеграционного тестирования, работу с моками. Как писать не хрупкие и надёжные тесты – о всём этом тут написано сполна.
Насколько Фаулер глубоко раскрыл практику рефакторинга, настолько же глубоко Владимир раскрыл практику unit-тестирования. Если вы хотите прочитать ровно одну книгу по этой теме и понять её фундаментальные основы, то вот ваш вариант. Я читал оригинал на английском, но книга есть и в русской версии. Возможно, даже она хорошо переведена, благо автор русскоговорящий.
В общем, Владимир меня приятно удивил. Под впечатлением от его книги мы в команде изменили подходы к unit-тестированию, и пока нас всё устраивает :-)
«xUnit Test Patterns: Refactoring Test Code». Gerard Meszaros
Если предыдущая книга больше всё-таки про принципы написания тестов, то эта больше про конкретные механники, которые помогают не сойти с пути, когда вы уже во всю начали их писать.
Одна из ловушек, в которую попадают новички, – это отношение к коду тестов как к неважному коду. При таком подходе, чем больше у вас становится тестов, тем больше шансов, что они могут начать «попахивать». Появляется сплошная копипаста тестовых данных, бесконтрольное усложнение кода проверок. И вот на мелкое изменение в функционале вы начинаете переписывать кучу сигнатур в коде тестов. Сетап теста начинает занимать 90% кода, файлы тестов увеличиваются с пугающей скоростью, merge request-ы, где менялись тесты, начинают превращаться в ад. И все надеются, что «да придёт Спаситель» и всё тут почистит :-)
В этой книге много про организацию тестовых данных, сложных проверок, тестовых дублёров. В общем, всё, чтобы не утонуть в тестах «at scale».
«Test-Driven Development By Example». Kent Beck
На сладкое, в теме про тесты нельзя не упомянуть отца unit-тестирования, создателя Extreme Programming.
Подход TDD до сих пор остаётся диковинной методикой разработки. По заявлению автора, с помощью него они разрабатывали большие системы. Но даже после прочтеня книги мне не до конца понятно, как именно у них это получалось. Я смотрел TDD Live Coding Sessions от разных разработчиков, но либо это были слишком простые примеры, либо люди прямо нарушали какие-то принципы из книги (например, начинали писать сервис с уровня HTTP запросов).
В книге разбирается 2 примера разработки приложений через TDD: калькулятор мультивалютных операций и фреймворк для unit-тестов. С помощью них автор показывает суть цикла «Red Test–Green Test–Refactoring», почему в нём важен порядок. Есть интересные рассуждения о том, какую проблему этим подходом автор пытался решить.
В общем, чтиво занятное, но скорее для искушенного читателя, который хочет ещё глубже залезть в эту нору :-)
PS
Как вы уже могли заметить, успешность рефакторинга зависит от успешности применения unit-тестов. Но успешность unit-тестов ввиду своих ограничений во многом зависит от вменяемости архитектуры приложения. Но об этом мы поговорим уже в следующий раз. А пока желаю вам приятного чтения!