К основному контенту

Про юнит-тесты и не только

Собрал в кучку разбросанные по чертогам памяти мысли про юнит-(и не только)-тестирование (копия недельного "марафона" из телеги, присоединяйтесь).

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 vs Chicago

London and Detroit schools of unit tests (похоже недоступно из РФ, есть копия из кэша)

Литература по тестированию (с акцентом на разработку)

• Если кому-то нужно про юнит-тесты на C++ (ну вдруг), то вот эта норм, я ее в свое время даже переводить собирался, так понравилась.

Не забывайте, ради чего все тесты пишутся


Комментарии

Популярные сообщения из этого блога

Полезные ресурсы для молодых (и не только) тестировщиков

сперто(с) Уже 3 месяца провожу собеседования тестировщиков (март 2016). Поначалу они просто  веселили - после 15-летнего опыта собеседования С++-разработчиков, общение с тестировщиками (чаще были "-цы") было чем-то экзотическим и забавным. Потом становилось все грустнее и грустнее, мимими закончилось. Началась печаль.

Mock vs Stub

Когда мы начали изучать модульное тестирование, то одними из первых терминов, с которыми пришлось познакомиться, стали Mock и Stub. Ниже попробуем порассуждать в чем их сходство и различие, как и для чего они применяются. Проверять работоспособность тестируемого объекта (system under test - SUT) можно двумя способами: оценивая состояние объекта или его поведение. В первом случае проверка правильности работы метода SUT заключается в оценке состояния самого SUT, а также взаимодействующих объектов, после вызова этого метода. Во-втором, мы проверяем набор и порядок действий (вызовов методов взаимодействующих объектов, других методов SUT), которое должен совершить метод SUT. Собственно, если коротко, то в одном случае используется Stub, а в другом Mock. Это объекты, которые создаются и используются взамен реальных объектов, с которым взаимодействует SUT в процессе своей работы. Теперь подробнее. Gerard Meszaros использует термин Test Double (дублер), как обозначение для объе

Заметки на коленке - 3. Что еще делать, если ваши тесты уже "зеленые"?

"Lately I find I'm working on automated tests that return non-binary results. Tests that neither pass nor fail" by  @noahsussman Отличная мысль, которую я ретвитил еще в 2016. Но давайте вместе подумаем, что за этим может скрываться? ( кстати, не знаю, что при этом думал Noah ) Ваши тесты прошли и прошли "успешно". Все хорошо или все же есть, куда еще посмотреть? Дальше то, что использовал я лично и то, что еще можно прикрутить дополнительно. Естественно все шаги ниже должны быть автоматизированны. 1. Контролируйте время выполнения тестов. Если набор проверок не меняется (а такое часто бывает, к сожалению), то рост времени выполнения может говорить о проблемах в продакшен коде (чаще всего) или проблемах с окружением. 2. Контроль за количеством выполняемых тестов. "Все зеленое" не значит, что сегодня выполняли те же Х тестов, что и вчера. Смешно(нет), но случается такое, что какие-то проверки "исчезают" из запуска из-за того, что у кого-то &qu