Собрал в кучку разбросанные по чертогам памяти мысли про юнит-(и не только)-тестирование (копия недельного "марафона" из телеги, присоединяйтесь).
TLTR: “Бей вперед - игра придет” (просто начните, если еще нет, хоть с какими-то автотестами).
Мне последние несколько лет нравится концепция сю-ха-ри, основная мысль которой заключается в том, что ты не можешь нарушать правила, пока не изучил базу, идеи и мысли других, придумал свои правила, а только потом можешь освобождаться от любых правил.
Если спроецировать этот подход на автоматизацию проверок, то получается, что сначала ты должен научиться писать тесты, как их видят в отрасли, а только потом точить подходы под себя.
Но фишка в том, что подходов очень много.
Например, кто-то считает, что модульные тесты мешают (недавние рассуждения Кирилла Мокевнина), кто-то считает, что интеграционные (интегрированные?) тесты - фигня, а модульные тесты рулят (JB Rainsberger Integrated Tests Are A Scam HD).
Каждый подход и мнение имеет в своей основе мысли, с которыми можно согласиться, с какими-то поспорить, но потом все равно прийти к философскому "зависит от".
Начнем с того, что многие в принципе не считают такие тесты классическими тестами. Оно и правда, если юнит-тесты написаны по канонам: “проверяется лишь одна функция одного юнита”, “все строго изолированно” и тп, то это лишь скорее просто инструмент разработчика.
Что он дает?
1. возможность запускать части кода без запуска всей системы, что обеспечивает:
• быструю обратную связь от вносимых изменений
• уверенность в вносимых изменениях
2. документация к коду
3. помощь в проектировании
Разберем коротенько по пунктам (заметки Капитана Очевидность, но я как-то не хочу их проскакивать)
1. Быстрый запуск кода - тут речь как про возможность “засунуть датчик” непосредственно в кишочки кода, так и про скорость выполнения самой проверки.
Когда у тебя есть возможность здесь и сейчас, без дополнительного окружения и ресурсов проверить, как выполняет свои задачи свеженаписанный код - это то, что нужно. К примеру, если функция по бинарному блобу определяет тип данных в нем, то не нужно проверять эту функцию запуская и дергая API сервиса, который где-то внутри себя использует эту функцию. Банальность и очевидность, но сплошь и рядом наблюдается (о причинах не сегодня). А если функция вызывается внутри десктоп приложения? Там с проверкой функции ”через приложение” вообще будет веселый аттракцион.
2. Документация. Есть интересная особенность памяти среднестатистического разработчика - она обнуляется по отношению к коду буквально через месяц (+/-). Некоторые только по истории гита определяют, что какое-то конкретное изменение кода было сделано именно ими. А уж про то, что именно код делает и для чего, так и подавно не помнят. Так вот тесты расположенные близко к коду (чаще всего те самые юнит-тесты), как раз помогают быстрее восстановить понимание предназначения кода и логики его работы. И да, конечно, это можно сделать и просто читая код. Но читая юнит-тесты, тем более хорошо написанные тесты с правильными названиями, сделать это намного быстрее. Тем более, что обычную документацию то редко кто пишет, а еще реже обновляет.
3. Проектирование. Использование юнит-тестов способствует (но не гарантирует) тому, что проверяемый ими код будет написан удобно, понятно, расширяемо. Потому что способность кода быть удобно тестируемым (тестируемость) напрямую связана с такими характеристиками качества кода, как изменяемость и поддерживаемость.
В этом месте можно было бы ввернуть про TDD (и я таки ввернул), но забавно, что сейчас это буквосочетание куда-то исчезло (только из моей инфоленты?). А то ли дело ра-а-аньше, трава зеленее, "TDD помогает" vs "TDD есть опиум для народа" и такие батлы в комментариях (тут тоже есть, если вы любитель попкорна).
Короче, юнит-тесты - это лишь инструмент разработчика: написал код так, чтобы потом его протестировать, проверил локально, остались какие-то артефакты имитирующие документацию. Работает в моменте здесь и сейчас.
Если юнит-тесты написаны канонично, то с вероятностью 99% их надо выкинуть (=сильно переписать) при изменении того кода, который они проверяют. И именно от последнего всех и бомбит.
Давайте немного вернемся к началу и поразмышляем на тему собственно юнита (модуля).
Что это такое? Какие тесты еще можно называть модульным, а какие уже не являются таковыми, потому что уже тестируют несколько юнитов?
Обратимся к классику (Мартин Фаулер) :
“Объектно-ориентированный подход рассматривает класс как юнит, процедурные или функциональные подходы могут рассматривать одну функцию как юнит. Но на самом деле команда решает, что такое юнит исходя из их понимания системы и ее тестирования" (вольный перевод, а статья кстати хорошая, почитайте)
Хмм, получается, что нет строгого определения, что считать юнитом?
Да, но есть нюансы.
Вы знали, что существует 2 школы боевого кунг-фу юнит-тестов?
Лондонская и Детройтская (она же Чикагская).
Что самое забавное, когда говорят про недостатки юнит-тестов, то чаще всего указывают на проблемы, которые возникают при лондонском подходе.
Чем же они отличаются?
Одно из отличий - это как раз определение размеров юнита.
Лондонская школа определяет юнит, как класс/функцию и предполагает изоляцию SUT (system under test) от всего того, с чем он взаимодействует через test doubles (тест-дублеры, на старославянском - моки, что неправильно, но все привыкли). То есть, мы проверяем строго то, как работает SUT ориентируясь на его публичный интерфейс и поведение нужное нам для реализации пользовательского сценария. А все его внешние зависимости замокированы. Это как раз и приводит к тому самому “цементированию” кода тестами, затрудняющего его модификацию.
В Детройтской школе юнит может быть любого размера, от класса до компонента из нескольких связанных между собой классов, при написании тестов избегают использования тест-дублеров и ориентируются в первую очередь на проверку состояния SUT.
Считается, что это "кунг-фу" несет риск появления кода, который потом не будет использоваться (нарушение принципа YAGNI), потому что мы не отталкиваемся от пользовательского сценария.
Но на моей практике, при сильном желании и достаточном умении, накостылить лишнего кода можно при любом подходе.
Мне больше импонирует детройтский подход, в первую очередь из-за низкой связности тестов с кодом (в сравнении с лондонским вариантом).
Итого: если отбросить в сторону #holywar со школами, то юнитом можно считать минимальное логическое объединение кода ориентированного на реализацию конкретной функциональности, которое мы хотим и можем проверить отдельно от других таких объединений.
При этом проектировать код нужно так, чтобы иметь возможность вносить нужную нам для тестов реализацию его зависимостей.
Заканчиваем.
Вредные советы:
• пишите юнит-тесты на чужой код после того, как код написан, просто для покрытия (мероприятие искажающее ценность таких тестов)
• не пишите юнит-тесты на чужой код (если тестов нет) перед тем, как меняешь код (зависит от объема изменений, возможно нужно писать другого уровня тесты)
• активно используйте привычные и традиционные программерские приемы типа удаления дублирования (приводит к ухудшению читаемости тестов, хотя пункт ооочень холиварный)
• продолжайте игнорировать книги по юнит-тестам и тестированию вообще
• открывайте вакансию “Unit Test Writer” (клянусь - реальность, погуглите, я рыдал, особенно над требованиями)
Полезные советы:
• почитать про мутационное тестирование
• всегда думать, какие тесты для чего лучше* подходят
• всегда думать (всех обнимаю)
*Критерии “лучшести”: скорость написания, скорость обратной связи (запуск, скорость выполнения и диагностика проблем)
По опыту: надо меньше зацикливаться на том, как тесты называются. А больше над тем, сколько они выполняются, как стабильно, какое окружение требуется и как быстро можно найти проблему, если что-то пошло не так.
Поэтому сильно плюсую к этому автору:
“Вообще мне кажется, что разделение на юнит, интеграционные, е2е и т.п. тесты устарело. Сейчас стоит придерживаться другой таксономии: тесты, которые можно прогонять при каждом пулл реквесте, и тесты, которые можно прогонять только собрав релиз.
И тестконтейнерс - один из способов перетащить часть тестов из второй категории в первую.”
Ну и уже от меня, само по себе разделение на “слои” провоцирует на разделение зон ответственности и появление вопроса “кто пишет”: типа разрабы это только юнит-тесты, а e2e - автоматизаторы. Еще один из примеров закона Конвея.
Полезные ссылки:
• Are You Chicago Or London When It Comes To TDD?
• London and Detroit schools of unit tests (похоже недоступно из РФ, есть копия из кэша)
• Литература по тестированию (с акцентом на разработку).
• Если кому-то нужно про юнит-тесты на C++ (ну вдруг), то вот эта норм, я ее в свое время даже переводить собирался, так понравилась.
Не забывайте, ради чего все тесты пишутся |
Комментарии
Отправить комментарий