Pull to refresh

Опыт работы с TDD и размышления о том, как надо тестировать код

Reading time 6 min
Views 9.8K
Недавно в нашей фирме устроили лекцию «Engineering Practices» и это оказалось введением в TDD.
О нет, только не это! «Иногда они возвращаются» (с) Стивен Кинг

На прошлой работе мы потратили 3 года на попытку внедрить эту методику. Это было мучительно. Менеджмент искренне верил в то, что TDD решит проблемы фирмы. Реальность разительно несоответствовала этому. Все это будило затертые воспоминания о Советской эпохе. Вспоминались висящие на стенах плакаты «Вперед к победе коммунизма» и фразы вроде «Учение Маркса всесильно потому, что оно верно».


Так что же не так в консерватории c TDD?

Дисклаймер


К сожалению, мой пост многими был понят в том смысле, что я против тестирования. И особенно против юнит тестов во всех их проявлениях. Это не совсем так, точнее это совсем не так.

Копирую сюда важное замечание, которое находилось в конце поста и оно не слишком бросалось в глаза.
Есть 3 вида кода, когда юнит тесты очень даже к месту и их использование оправдано:
  1. ПО повышенной надежности — к примеру, ПО для самолетов, электростанции и т.п.
  2. Легко изолируемый код – алгоритмы и все такое
  3. Утилиты – код который будет ОЧЕНЬ широко использоваться в системе, так что стоит их досконально прошерстить заранее

Если вам легко и просто писать юнит тесты, что означает, что ваш код легко изолируем, то вам совсем не обязательно читать дальше и вникать в мой опыт, выстраданный болью, потом и кровью. Ну а если, нет, то имейте в виду, что нижеописанное — исключительно единичный кейс, но зато выстраданный и осмысленный.

Для начала, определимся с терминами.

Терминология


Ручные тесты (Manual tests)
Функционал тестируется вручную на «живой» системе, использую стандартный для нее UI.
Это обычный, традиционный метод тестинга для QA.

Функциональные тесты (Functional tests)
Программист пишет некое «тестовое» UI, которое позволяет запускать определенные сценарии на «живой» системе, с взаимодействием настоящих модулей.
Это весьма распространенный метод тестирования, используемый программистами.

Юнит тесты (Unit tests)
Программист пишет тесты, которые способны выполняться в «изолированной» среде – без других модулей. Прочие модули, к которым в процессе работы, должен обращаться тестируемый код, заменяются на на Моки (mocks). Таким образом, тесты могут исполняться в автоматическом режиме, на билд-машине, после компиляции.
Существует тренд на использование таких тестов. И программисты испытывают давление (моральное и административное), с тем чтобы перейти на их использование.

Отдельное замечание насчет TDD
TDD – это методика программирования, широко использующая юнит тесты. И если с Юнит тестами что-то не так, то TDD может к этому добавить неприятностей (что он с успехом делает), но отнюдь не убавить.

«Ошибка в коде»
Код написан с ошибкой и делает нечто, отличное от намерения программиста.

«Ошибки интеграции»
Два программиста писали взаимодействующие модули/классы, и неправильно понимали протокол. Под протоколом понимаются как параметры, передаваемые в функции, так и правильную/допустимую последовательность вызовов.
Причем, каждый из классов может быть написан «чисто», но собранные вместе они дают теправильную работу системы.

«Ошибки мультисрединга и тайминга»
Если в работе учавствуют несколько тредов (threads), возможны ошибки, связанные с доступом к общему ресурсу или с последовательностью вызовов. Это самые трудноуловимые (после memory corruption) баги, с которыми мне доводилось сталкиваться.

«Ошибки UI» и «Ошибки непредвиденных ситуаций»
UI не соответствует требованиям вообще или как-то криво обрабатывает некие специфические ситуации.
Скажем, допускает ввод некорректных данных или не отображает достаточно понятное сообщение в случае отрицательного результата.

Плюсы и минусы различных видов тестирования


Manual Functional Unit
Возможность находить «ошибки в коде» Да Да Да
Возможность находить «ошибки интеграции» Да Да Нет
Возможность находить «Ошибки мультисрединга и тайминга» Частично Частично Нет
Возможность находить «ошибки UI» и «ошибки непредвиденных ситуаций» Да Частично Нет
Возможность тестировать с разнообразными входными данными Низкая Приемлемая Очень высокая
Возможность автоматизировать тестирование Очень низкая Низкая Да
Дополнительные усилия в программировании Нет Нет… x1.5 x2… x5


Согласно моему опыту (точной статистики не имею, к сожалению), «ошибки в коде» составляют менее 30% всех багов в достаточно сложной системе. Если программист достаточно опытен, количество таких ошибок будет еще меньше.
Подавляющее большинство таких ошибок отлично ловятся с помощью «инспекций кода» (code review) и функциональных тестов.

Юнит тесты способны тестировать код с самыми разными возможными вариантами, вплоть до всех мыслимых входных данных. Можно протестировать код очень тщательно, можно даже «до последней запятой».
Проблема в том, что существует некий набор ситуаций, котрые могут случиться в реальной системе. Тестирование чего-то сверх этого набора нерелевантно и является потерей времени.
Обычно функциональные тесты покрывают вполне «рациональный» набор возможных ситуаций.

Наиболее сильная сторона юнит тестов – автоматизация.
Но вот лично я (за 3 года работы) не сталкивался с ситуацией, когда юнит тесты помогали найти баг спустя достаточно большое время после его написания.
То есть юнит тесты эффективны (помогают найти баги) исключительно короткое время после написания.
Я ни разу не видел, чтобы автоматически запускаемые на билдере юнит тесты что-то отловили. Они там «чистят чистое». Ну и, конечно, служат душевному спокойствию руководства. :)

Минус юнит тестов – громадные усилия, которые приходится затрачивать на написание и программирование моков. Именно поэтому время, потребное на разработку с использованием юнит тестов в 2-5 раз больше чем без них.

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

Резюме:
Юнит тесты не дают значительного выигрыша в надежности системы.
Есть 3 вида кода, когда юнит тесты очень даже к месту и их использование оправдано:
  1. ПО повышенной надежности – здесь никаких затрат не жалко
  2. Легко изолируемый код – писать юнит тесты легко, просто и затраты на них ничуть не больше, чем у функциональных тестов
  3. Утилиты – код который будет ОЧЕНЬ широко использоваться в системе, так что стоит их досконально прошерстить заранее
Во всех остальных случаях, соотношение выигрыш/затраты у юнит тестов довольно низкое. Существуют гораздо более дешевые способы борьбы с теми багами, которые ловятся с помощью юнит тестов (в частности, инспекция кода).
Если стоимость ошибки исключительно высока (к примеру, пишем ПО самолета или электростанции), то, разумеется, никакие затраты не черезмерны и юнит тесты надо обязательно использовать. Но обычно цена ошибки значительно меньше.


Организацинные и психологические моменты


Вопрос на засыпку: кто и как может убедиться, что программист написал юнит тесты достаточно хорошо?

Существует два возможных варианта проверки юнит тестов:
Инструменальная проверка
Существует возможность измерить «покрытие» кода тестами – все ли строчки кода выполнялись в рамках тестов. Я ни разу не видел, чтобы такая проверка использовалась. Ну и, разумеется, такие тесты могут проверить, что тесты «покрывают» код, но не могут проверить, что тесты «покрывают» возможные входные данные.

Инспекция юнит тестов другим программистом или руководителем
Я видел как это делается. Писать юнит тесты – весьма занудное дело. Разбираться в них – вообще мрак. То есть, можно разобраться, но это требует серьезного количества времени и мотивации. В самом начале внедрения, у людей был интерес и мотивация сделать все как надо, чтобы разобраться что это такое – юнит тесты и TDD. Но очень скоро это прошло.

Таким образом, проконтролировать программиста, что он написал юнит тесты хорошо, довольно сложно.
Как только контроль и административное давление снижается, программисты, вот лентяи, склонны писать юнит тесты все более «для галочки» и тестировать свой код с помощью ручного или функционального тестирования.
Для систем с не слишком большой стоимостью ошибки, программисты не испытывают достаточного давление чтобы быть параноиками для честного использования юнит тестов.

Если программист упускает что-то в коде, ее ловят в QA или уже у клиента и баг возвращается для исправления. Обратная связь заставляет относиться к коду серьезно и внимательно.
Если программист упускает что-то в написании тестов, это фактически никто никогда не заметит. Отсутствие обратной связи позволяет относиться к вопросу наплевательски.
Даже если программист получил в пять раз больше времени на разработку, я он скорее пойду пойдет почитать Хабрахабр, вместо того чтобы дотошно выпиливать лобзиком тесты, моки, тесты, моки… и так много дней подряд.

Как же еще можно заставить юнит тесты «работать»? Они должны работать если тесты будут писаться другим программистом, причем лучше всего до самого кода.
И тут мы приходим к TDD.

Test Driven Development


Если тест написан до и предоставлен программисту в качестве исходных данных, он становится «техническми требованием» (requirement). Разумеется, тот кто пишет тест, должен заранее написать и запрограммировать все требуемые моки. Ха-ха, не хотел бы я быть тем программистом, в обязанности которого входит программировать тесты для других.

Ну, а может ли сам программист написать тесты к своему коду заранее?
По моему опыту, нет и нет. Программисты (и я и подавляющее большинство, с кем я это обсуждал) просто думают в обратном направлении.

Да, поначалу люди пытались честно следовать методологии. Но спустя какое-то время, начинали сначала писать код, а потом на него дописывать тесты. А еще спустя какое-то время, тесты потеряли свою разносторонность и дотошность. А потом и вовсе писались «штоб было», раз уж просят.

А оно вообще надо?
Tags:
Hubs:
-14
Comments 69
Comments Comments 69

Articles