OpenQuality.ru

Качество программного обеспечения

Качество программного обеспечения: в главных ролях

Лента  Радар  Блог  Опыт  
Разум  Видео  Заметки  Эпизоды


Модульные тесты: pros, cons, et cetera

Добрый день.

 
Заметки о модульном тестировании: терминология, аргументы, контраргументы, точки над i, рекомендации.
 

• Модульные тесты и TDD
• Аргументы в пользу модульных тестов
• Аргументы против модульных тестов
• В каких случаях модульные тесты будут полезны
• В каких случаях модульные тесты будут неэффективны
• Рекомендации по созданию модульных тестов

 
Модульные тесты и TDD

Модуль, в контексте модульного тестирования, представляет собой минимальную смысловую единицу исходного кода, пригодную для тестирования (функция, процедура, метод, в отдельных случаях – класс). Модульный тест проверяет работоспособность модуля в условиях изоляции от других модулей приложения. Исключив влияние других модулей, мы проверяем наш модуль в стерильных условиях: если результат выполнения модульного теста не соответствует ожидаемому, то ошибка кроется в логике нашего модуля, а не во внешних факторах.

Изначально, основным назначением модульного тестирования были регрессионные проверки: меняются требования к продукту → вносятся изменения в исходный код → нужно проверить, что введение новой функциональности не привело к поломке старой. Модульные тесты служили (и служат) своего рода датчиками состояния продукта: “ага, вот тут что-то сбоить стало, не иначе как изменения в методе A класса B поломали кэширование http-запросов”. Все просто и логично. Но, как оказалось, unit-тесты способны на большее.

Вторым рождением модульные тесты обязаны череде нововведений в методиках создания ПО. Водопадная модель, появившаяся на свет в каменном веке ИТ, перестала соответствовать требованиям века бронзового. Не каждый проект мог себе позволить многомесячный цикл проектирования-разработки-тестирования, при котором спецификация продукта оставалась неизменной. К моменту своего созревания продукт мог оказаться несъедобным в том виде, в котором задумывался: появились сильные конкуренты, изменились запросы потенциальных клиентов. Как в одном из эпизодов Ералаша: мальчик-копуша так долго собирался пойти играть с друзьями в футбол, что не заметил наступление зимы, когда впору выходить на лед с клюшкой и гонять шайбу до темноты. Появилась потребность в более гибких методиках, позволяющих оперативно реагировать на меняющиеся требования рынка. В чем заключается гибкость? В том, что полный цикл создания продукта разбивается на итерации, каждая из которых представляет собой “водопад в миниатюре”: в качестве цели выбирается не полная готовность продукта, а реализация того или иного требования к нему. Если в случае “большого водопада” у нас было две стадии (“нет продукта”, “есть продукт” через год), то в случае каскада “малых водопадов” у нас есть продукт после каждой итерации. Он не содержит всей задуманной функциональности, но его можно выпустить в свет уже сейчас или внести изменения в планирование следующих итераций в соответствии с состоянием рынка.

Как сделать каждую итерацию максимально эффективной? Не делать ничего лишнего. Есть требование к продукту, его надо реализовать с минимальными затратами времени и сил. Для этого разработчику нужно четко представлять конечный результат на языке программного кода. Один из вариантов такого представления – модульные тесты, сердце TDD. Разработчик пишет тесты на новую функциональность. Первоначально прогон каждого теста завершается неудачно (тест существует, но кода, который он проверяет, нет). Разработчик пишет код приложения и снова прогоняет тест. Если тест пройден успешно (равно как и все ранее написанные тесты на “старую” функциональность), промежуточная мини-цель считается достигнутой.

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

 
Аргументы в пользу модульных тестов

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

2. В случае TDD: ожидаемый результат выполнения кода. Модульные тесты описывают прототип приложения – заложенную структуру, базовые алгоритмы. Соответственно, код реализует идеи разработчика, выраженные в модульных тестах (перевод бизнес-языка в язык программного кода).

3. В случае TDD: возможность не делать лишних движений. Модульные тесты соответствуют бизнес-требованиям → код прошел модульные тесты → задача выполнена.

4. Возможность проводить рефакторинг без опасений поломать работу приложения и погрязнуть в отладке (по некоторым оценкам, 80% своего времени разработчик тратит на работу с gdb, windbg и подобными инструментами). Основное назначение модульных тестов состоит не столько в том, чтобы находить баги в текущей версии продукта, сколько в том, чтобы не пропустить баги в старой функциональности при добавлении новой. К примеру, маленькое “улучшение” в алгоритме шифрования не разрушит всю систему, если на алгоритм и смежные компоненты есть модульные тесты.

5. Скорость нахождения багов. Будут найдены быстрее. По оценкам экспертов, модульные тесты позволяют найти около 15% багов, выявленных в ходе полного цикла разработки. В то же время, большей частью это критические баги, и их раннее выявление способно сберечь существенную долю ресурсов и предотвратить катаклизмы в среде пользователя программного продукта. Чем короче путь в исправлении бага, тем меньше будет затрачено усилий на его исправление. Если баг обнаружен сразу, то жизнь его будет яркой, но недолгой. Если же баг прорвется дальше, то он хорошо проведет время: пока его найдет и научится воспроизводить тестировщик, пока будет принято решение о том, что с ним делать, пока он попадет к разработчику, пока будет изучен и, возможно, устранен. Безусловно, написание тестов требует времени, особенно на этапе овладения этим навыком. Но чем больше опыт, тем быстрее будет движение вперед – не столько в написании тестов, сколько в развитии всего продукта. Новая функциональность будет внедряться быстрее, без досадных разбирательств о том, что могло привести к поломке старого кода.

6. Возможность тестирования базовой функциональности без UI. Хороший пример: SaaS-системы. Если в базовой части приложения, работающего на платформе Windows Azure, проявляется сбой при большом количестве записей в таблице пользователей, то гораздо легче воспроизвести (и впоследствии перепроверить) этот баг в локальной среде с помощью mock-объектов. Следует также отметить, что это будет более экономное решение. Никто не позволит отлаживать баг в “живой” системе, обслуживающей реальных пользователей. Значит, при отсутствии модульных тестов понадобится вспомогательная площадка, за которую нужно будет платить.

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

8. Модульные тесты служат дополнением к документации и помогают новому разработчику войти в курс дела. Объектно-ориентированные языки программирования предполагают наличие зависимостей между классами. Небольшое изменение в, казалось бы, второстепенном классе, способно разрушить работу всей системы, поэтому модульные тесты в таких случаях могут оказаться очень полезны.

9. Точечная настройка производительности системы и обнаружение утечек памяти. Воспроизвести 100 тыс. одновременных подключений и выполнение определенной последовательности действий в системе не так-то просто. Гораздо удобнее воспроизвести эту нагрузку с помощью модульных тестов и наблюдать поведение продукта с помощью purify, valgrind и подобных инструментов. Можно пойти дальше и рассматривать результаты замера производительности как критерий оценки внесенных изменений. Если при добавлении новой функциональности производительность системы ухудшилась, то это может служить красным светом для внедрения данной версии продукта.

 
Аргументы против модульных тестов

1. Время – деньги. Деньги – время. Вместо написания кода, который можно продать, разработчик будет трудиться над тестами. Сама идея предохраняться от всего и вся может оказаться надуманной. Лучше по факту посмотреть, что сломалось, и чинить именно эту часть.

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

3. Модульные тесты выполняются “в вакууме”, в стерильных условиях. Заглушки (stubs) и имитаторы (mocks) – удобная штука, но это лишь эмуляция поведения системы. В реальной жизни код может валиться, скажем, из-за проблем с производительностью, причем именно в том месте, где, казалось бы, модульные тесты и не нужны.

4. Модульные тесты – это не серебряная пуля. Они покажут наличие ошибок, но не докажут их отсутствие.

 
В каких случаях модульные тесты будут оправданны

1. Бизнес-требования меняются часто, код меняется часто (добавляется новая функциональность, проводится рефакторинг, выполняется починка багов). В этом случае модульные тесты – это спасительная соломка, позволяющая без опасений двигаться вперед.

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

3. Высококритичные системы (медицина), системы повышенной опасности (ядерные объекты). Семь раз отмерь, один раз отрежь.

4. Существует потребность быстро получить оценку текущей работоспособности системы. Запустил, перекурил, получил.

5. Возможность чинить баги с минимальными издержками (как в описанном выше случае с Azure).

6. Код пришел всерьез и надолго. Он может быть не супер-пупер-критичен, но его предполагается широко использовать. Модульные тесты повышают его значимость. Один из примеров: фреймворк .Net, который применяют разработчики по всему миру.

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

1. Код-однодневка. Заказ на аутсорсинг: сдал – и забыл. Модульные тесты отнимут время и деньги.

2. Идея модульных тестов не принимается частью разработчиков. Надежность системы равна надежности самого слабого звена.

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

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

5. Потемкинская деревня. Не нужно писать модульные тесты “для галочки” – пользы никакой.

 
Рекомендации по созданию модульных тестов

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

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

3. Модульные тесты полезны для проверки сценариев, которые трудно или дорого воспроизвести при тестировании через пользовательский интерфейс.

4. Особое внимание логике приложения (if, for, while и т.п.). Если у продукта будет отсутствовать меню, это будет заметно сразу. Проверить все логические ходы-выходы гораздо сложнее.

5. Нужно минимизировать время прогона модульных тестов. Они должны выполняться быстро, иначе в них нет смысла. Поэтому каждый модульный тест должны быть достаточно “скоротечным”.

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

7. Модульный тест должен быть простым, охватывать 1-2 метода. Если зажегся красный свет, разработчику сразу должно быть ясно, где собака зарыта.

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

9. В модульных тестах желательно обходиться без обращения к базам данных, сокетам и прочим объектам “внешнего мира”, иначе это будет не модульный, а интеграционный тест. Напомним, что тест должен выполняться быстро, иначе в нем нет никакого смысла. Это значит, что в модульном тесте не должно быть никакого ввода-вывода. Могут оказаться полезны заглушки (stubs) и имитаторы (mocks).

10.В написании модульных тестов нет одного, единственно верного пути. Железная логика одного подхода может разбиться о неприступные скалы другого. Кто-то считает методы SetUp and TearDown неотъемлемым атрибутом модлуьного теста, а кто-то предлагает обходиться без них. Кому-то нравятся имитаторы, а кто-то придает большее значение заглушкам. Кто-то обходится без TDD, но при этом придает важное значение модульным тестам. Кто-то положительно отзывается о модульных тестах, но при этом видит, что полезны они не всегда. Нужно искать свой путь, исходя из контекста своего продукта. Модульные тесты – это инструмент разработчика. Если разработчик знает, как написать свой код, то он знает, какие тесты ему наиболее важны.

11. Модульные тесты – это основание пирамиды тестирования, проверка базовой функциональность, отлов логических ошибок. В деле обеспечения качества продукта они необходимы, но недостаточны. Над ними располагаются функциональные тесты – к примеру, охватывающие взаимодействие с базами данных или объектами в Интернете. Затем следуют интеграционные тесты, проверяющие взаимодействие компонентов системы. Далее идут приемочные тесты с позиции пользователя (воспроизвести базовые сценарии, по которым будет действовать пользователь системы). И наконец, исследовательское тестирование – вершина пирамиды и полновластная епархия тестировщика.

12. При внедрении модульных тестов важно избежать показухи и насилия. “Демонстрационные” модульные тесты не принесут никакой пользы, а “обязаловка” может встретить серьезное противодействие. Лучше выслушать аргументы против модульных тестов. Возможно, разработчик окажется прав, не желая создавать модульный тест при каких-то обстоятельствах. К примеру, тот или иной метод может оказаться слишком сложным или неудобным для модульного теста. Тогда надо понять, как этот метод будет тестироваться. Возможно, будет достаточно анализа кода (code review). Возможно, эта функциональность будет проверена в интеграционных тестах. Возможно, стоит предупредить тестировщиков, чтобы на тот или иной сценарий они обратили особое внимание. Или другой вариант: если метод сложно тестировать, то, возможно, его следует разбить на несколько более простых.
 
И в заключение две метафоры.

1. Представьте, что вы набрали избыточный вес и хотите от него избавиться. Вы идете в тренажерный зал, тренируетесь на беговой дорожке. На следующий день болят мышцы, кушать хочется еще больше. Два варианта дальнейших событий: a) да ну эти нагрузки, не такой уж я и толстый; б) потерплю – дальше будет легче. В первом случае вы будете продолжать набирать вес, во втором – войдете в ритм тренировок и приобретете хорошую физическую форму. То же самое с программным кодом, для которого модульные тесты служат средством его улучшения. Потребуется время на привыкание, но потом создавать модульные тесты станет легче, а польза будет все более ощутимой.

2. Мартин Фаулер описывает методику обучения Shu-Ha-Ri, пришедшую из айкидо. На первом этапе ученик следует указаниям мастера, в точности воспроизводя его действия. На втором этапе он начинает их осмысливать и параллельно черпать вдохновение в работах других мастеров. На третьем этапе ученик начинает полагаться на свой опыт, разрабатывает свои подходы и рассчитывает на свои силы. То же самое с модульными тестами: следование рекомендациям гуру, их осмысление и выработка своих практик исходя из специфики своего приложения.

До встречи. Оставайтесь с нами.

Отправить в Twitter, Facebook, ВКонтакте | Опубликовано 16.08.2010 в рубрике "Модульные тесты"

Комментарии (2)

  1. Pingback : Качество программного обеспечения: невозможное возможно : Модульные тесты: pros, cons, et cetera | August 16, 2010

    […]     Добрый день.Модульные тесты и TDD. Аргументы и контраргументы. В каких случаях модульные тесты будут полезны? При каких условиях шкурка выделки не стоит? Практические рекомендации. Читать далее. […]


  2. Pingback : OpenQuality.ru | Качество программного обеспечения | April 1, 2012

    […] функциональности. У модульных тестов есть свои плюсы и минусы, но в целом они способны более эффективно решать часть […]



Добавить комментарий

Пожалуйста, исправьте результат: дважды два равно



КРАТКОЕ СОДЕРЖАНИЕ

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


ПУТЕВОДИТЕЛЬ

Проект был основан в 2008 году. За это время часть статей устарела, а некоторые из них вызывают улыбку, но пусть они останутся в том виде, в котором были написаны. Cписок всех статей с краткой аннотацией и разбивкой по рубрикам: открыть.

ПОДПИСКА

Доступ к самым интересным материалам по электропочте и RSS. Подробности.

ИЩЕЙКА