...почему модульные тесты пропускают баги?
Image source: Shippable

...почему модульные тесты пропускают баги?

Недавно я писал о выпуске дополнительного релиза Selenum с исправлением багов, проскочивших в основной релиз, и там была фраза, которая вызвала возмущение читателей в фейсбуке: “практика показала, что модульных тестов явно недостаточно”. Мол, это не тесты виноваты, а те, кто их написал. Надо писать хорошие тесты, тогда они не будут пропускать баги. С этим не поспоришь, однако возникает вопрос – почему мы написали плохие тесты? Ведь не специально же мы это сделали. Старались написать хорошие тесты, а они баги не ловят. Почему? Давайте попробуем с этим разобраться на одном конкретном примере.

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

Один из пропущенных багов состоял в том, что Selenium Server “портил” целые числа, передаваемые в запросах, превращал их в числа с плавающей точкой.

Интеграционные тесты этот баг ловили, а модульные – нет. Почему?

Упаковку данных в формат JSON выполняет самодельный класс BeanToJsonConverter, для него есть модульные тесты, в том числе есть тест, который проверяет корректность обработки целых чисел. Распаковывает данные класс JsonToBeanConverter, и для него тоже есть соответствующий модульный тест. Оба они являются обертками вокруг библиотеки gson, устраняют ее недостатки и добавляют новые возможности.

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

Выяснилось, что в одном месте в коде Selenium Server, в классе ProtocolHandshake, вместо обертки JsonToBeanConverter был использован непосредственно класс Gson, который как раз портит целые числа!

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

Тот фрагмент кода, в котором использовался неправильный преобразователь формата JSON, тоже благополучно покрыт модульными тестами, проверяющими, что преобразователь срабатывает. Но… В этих модульных тестах ни разу не встречались целые числа, а на тех данных, которые использовались, Gson и без всяких оберток отработал правильно.

Почему мы решили, что созданные модульные тесты “достаточно хорошие”?

Потому что для оценки качества модульных тестов традиционно используется метрика покрытия строк кода.

Все нужные строки кода в классе JsonToBeanConverter покрыты. То же верно и для класса ProtocolHandshake. Каждая строка хотя бы один раз отработала при выполнении тестов.

Нужно ли было писать тест для ProtocolHandshake, в котором были бы задействованы целые числа? С точки зрения используемой метрики – нет, не нужно. Этот дополнительный тест не увеличивает покрытие кода, он лишний, избыточный. Хотя именно он мог бы поймать пропущенный баг.

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

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

Как же быть?

Нужно просто смириться с этим.

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

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

Зато модульные тесты работают быстро. За это мы их и любим :)


Алексей Баранцев

Автор:

Если вам понравилась эта статья, вы можете поделиться ею в социальных сетях (кнопочки ниже), а потом вернуться на главную страницу блога и почитать другие мои статьи.
Ну а если вы не согласны с чем-то или хотите что-нибудь дополнить – оставьте комментарий ниже, может быть это послужит поводом для написания новой интересной статьи.

Мои тренинги
А ещё есть? Конечно!