целью использования метрики покрытия кода является
Code Coverage — хочу верить
Разработчик обязан знать свои инструменты! Знание инструментов увеличивает продуктивность, эффективность, производительность, потенцию разработчика! Не могу программировать без R#!
Подобного рода фразы можно услышать от абсолютно разных людей: фанатиков разработки, продавцов различных утилит, пользователей удобных тулз. Слышит их и мой менеджер, когда мне хочется поэкспериментировать с чем-то новеньким.
Правда, инструкция к инструменту обычно не содержит раздел «Противопоказания», не указываются ситуации когда НЕ стоит применять утилиту. Между тем, подобный раздел мог бы сэкономить тонны времени на неудачные эксперименты.
Сегодня я пошвыряю камни в огород Code Coverage (CC). Достаточно полезная метрика, под которой лежат несколько скудно документированных граблей.
«Есть ложь, есть наглая ложь
есть статистика».
Создатели SonarQube это великолепно понимают, недаром у SonarQube с десяток CC метрик. Я буду перебивать статистику используя CC от DevExpress, там метрика лишь одна.
Проблема 1. Типичный тест не того. Протестируем метод с кучей проверок аргумента:
Метод покрыт тестами на 83%, чего обычно достаточно для авто-билда. Технически спорить не о чем, большая часть кода покрыта тестами, но основной сценарий тестами не затронут. Тестами покрыта наиболее простая часть кода, не наиболее важная.
Проблема 2. Замеряем актуальный код, вместо необходимого.
Тестируемый метод не содержит проверки на null аргумент, однако покрытие — 100%. Иногда люди забывают: Code Coverage — это метрика покрытия кода, не метрика закрытия требований; если в методе не достает логики (метод недостаточно сложен для решения своей задачи) — CC это не покажет.
100% покрытия не гарантируют работоспособности программы. Доводя до абсурда: пустой метод элементарно покрывается на 100%. Непустой метод покрывается на 100% тестами без Assert-ов.
Проблема 3. Оптимизм. Немного иное проявление предыдущей проблемы. Как видно, один тест покрывает 100% кода. Попробуем переписать наш метод, избавившись от LINQ (для улучшения производительности).
Получаем лишь 73% покрытия. Функциональность не изменилась, метрика упала. Мало того, что 100% покрытия не гарантируют работоспособности программы, эти 100% могут быть фейковыми. Вывод: LINQ — г**но результаты CC могут быть завышены, старайтесь проверять покрытие в редакторе.
Следствие: Используемый инструмент может не обладать всей желаемой функциональностью. Тривиальная вещь, не менее от того верная.
Проблема 4. Передача ответственности.
Метод покрывается на 100% одним тестом. При этом покрытие OuterLib библиотеки лежит на совести того, кто её добавил. Или обновил. Года три назад, до введения CC. До увольнения.
Приходится снова констатировать факт: мало того, что 100% покрытия не гарантируют работоспособности программы, эти 100% могут быть фейковыми.
Помимо чисто кодовых моментов есть несколько претензий именно к обработке результатов CC
Претензия 0, всем известная. 100% покрытия. Нет 100% покрытия — нет одобрения билда. Проблема в том, что первые проценты покрытия получить относительно просто, а вот последние… Особенно, когда часть кода генерируется. Или недостижима (поскольку создана для Васи, который будет её юзать через два дня). Или просто теоретически достижима, а пример подбирать\высчитывать пару недель (такое бывает при работе с математикой). Короче, большинство команд (из тех кто вообще интегрирует CC в CI) останавливаются на 60\70\80 процентах необходимого покрытия.
Претензия 1, спорная. Покрытие мертвого кода. На моей памяти схожая проблема особо ярко проявилась в ходе проверки Mirand-ы коллегами из PVS. Комментарии довольно эмоциональны, но часть споров касалась мертвого кода: часть найденных диагностик указывала на (заброшенные) плагины, но не на ядро.
Возникает вопрос: нужен ли CodeCoverage для мертвого кода? С одной стороны, мертвый код это проблема, и привлечение внимания к нему приветствуется. С другой стороны, мертвый код не влияет на продакшн, так стоит ли позволять ему влиять на CC метрику?
Претензия 2. Важность кода. Расширение проблемы 1. В моем проекте есть два примечательных контроллера: «оплата» и «переговорка». «Оплата» критична для клиента, и я вполне согласен с требованием «80% покрытия», «переговоркой» же пользуются 1.5 анонимуса. В год. И она не менялась уже два года. Вопрос: для чего писать тесты к полумертвой функциональности? Лишь для получения 80% бейджа одобрения автосборки?
Претензия 3, невозможная. Метрика как ачивка. Это когда никто не проверяет что именно покрыто. Помните байки про оплату за линии кода? Мне доводилось слышать про людей, которые творили ненужный кода для лучшего покрытия.
Претензия 4. Метрика «за бесплатно». Когда руководство скидывает требование «покрывайте код на 80%», и разработчики безропотно соглашаются. Проект при этом — одноразовый. Или прототип. Или дедлайн на носу. Или имеется здоровенный макаронный легаси монстр без единого теста.
Покрытие кода тестами требует времени! Если покрытие еще и замерять — время на тесты может и возрасти (хотя может и упасть). Так что если команда не успела сдать проект в срок, но зато достигла 80% покрытия — вина может поделиться между руководством и разработчиками. Вопрос линии раздела вины поднимать не стоит, ибо холивар.
Под конец. Еще раз замечу: СС — метрика полезная, хоть и с сюрпризами. Она реально помогает с контролем кода, если нет слепого стремления к цифрам в отчетах.
О метриках тестирования: code coverage для тестировщиков
Как известно из книги «Путеводитель для путешествующих автостопом по галактике», ответ на главный вопрос жизни, вселенной и всего такого — 42. Процент покрытия кода по линиям на одном из моих проектов — 81, дает ли эта цифра ответ на главный вопрос тестирования «cколько тестов достаточно для определения качества продукта»?
В течении своей работы в айти-сфере и тестировании я видела мало команд и проектов, где тестировщики реально используют code coverage в своей работе. Связано это на мой взгляд с двумя вещами:
1. Тем, что тестируем мы прежде всего требования;
2. Далеко не все понимают, как считать и использовать покрытие.
Интересующимся предлагаю свой взгляд на эти 2 пункта.
Требования vs код
Тестировщик тестирует требования. Даже если их формально нет, есть представление о том, как должна вести себя система. Это и только это важно в конечном итоге.
Но.
Не бывает четких исчерпывающих полных требований, проверив каждое из которых, смело можно сказать, что система будет работать как надо и багов нет.
Пример 1
Приложение пытается сохранить данные в БД (располагается на другом сервере). Есть описание того, как оно должно это делать, в том числе звучит требование, что в случае невозможности выполнить операцию (нет доступа к БД, например), мы должы пытаться это сделать до истечения определенного таймаута, потом выдавать клиенту ошибку.
Что значит невозможно выполнить операцию?
Предположим, тестировщик проверяет сценарий с потерей соединения к БД в процессе работы. Все работает хорошо, но значит ли, что багов нет?
В упомянутом приложении мы посмотрели покрытие кода соответствующих классов — оказалось, что разработчик предусмотрел в коде обработку около 5 исключительных ситуаций.
Это значило, как минимум, следующие случаи:
1. Соединение с сервером БД не может быть установлено;
2. Соединение с сервером БД установлено, выполнение запроса вызвало оракловую ошибку;
3. Соединение с сервером БД было установлено, запрос начал выполняться и завис — тут был баг. Приложение ждало ответа примерно минут 5, потом в логи летел эксепшн и больше оно эти данные записать не пыталось.
Пара остальных не стоило внимания по разным причинам.
В примере требования формально проверено было и 1-м кейсом, но баг был найден после анализа покрытия кода. Можно поспорить, что это пример не о пользе code coverage, а о пользе взаимодействия внутри команды (у разработчика детали имплементации можно было бы узнать заранее или дать ему кейсы на ревью), на самом деле я всегда так делаю но не о всем догадаешься спросить, часто внимание к каким-то вещам привлекают непокрытые блоки кода.
Пример 2
В другой системе, которуя я тестировала, при потере консистентности данных приложение должно было выкидывать соответствующий эксепшн, бросать нотификацию мониторингу и ждать, когда придут люди и спасут его. Тесты покрывали разные случаи возникновения таких ситуаций, все обрабатывалось нормально.
Мы посмотрели код, нужный кусок был покрыт хорошо, но я увидела в другом классе непокрытую область кода, в которой бросался тот же самый event о потери консистентности. При каких условиях — неизвестно, т.к. разработчики его быстро выпилили. Оказалось он был скопипасчен из старого проекта, но никто об этом не помнил. Где это могло стрельнуть- неизвестно, но без анализа кода мы бы это не нашли.
Поэтому пусть тестировщик тестирует требования, но если он смотрит еще и код, может поймать то, что в требованиях не описано и хитрые методы тест-дизайна тоже не всегда найдут.
Покрытие = 80. А качество?
Количество не означает качество. Оценка покрытия кода напрямую не связана с качеством продукта, но связана опосредованно.
На одном отчетном совещании я заявила, что покрытие кода у нас увеличилось до 82% по линиям и 51% по условиям, после чего руководством мне был задан вопрос: «А что это значит? Это хорошо или плохо?» Закономерный вопрос, действительно: сколько надо, чтобы было хорошо?
Некоторые разработчики покрывают свой код, добиваясь 100%. Тестировщику 100% добиваться бессмысленно, начиная с какого-то моменты вы столкнетесь с тем, что физически не можете затронуть этот код интеграционными тестами.
Например, разработчики считают хорошим тоном проверять входящие параметры метода на null, хотя в реально работающей системе таких случаев может и не быть (50% по условиям у нас тогда складывалось в том числе из-за этого). Это нормально, передать туда null извне можно было только до первой проверки, которая собственно эту ситуацию и обработает.
К вопросу об «это нормально»: качественная оценка непокрытого кода и ведет в моем понимании к адекватному использованию code coverege. Смотреть важно то, что вы не покрыли, а не сколько. Если это java-код и методы toString(), equals() или ветви с exception, которые сложно воспроизвести интеграционно, ну так и ладно, пусть будет 80% покрытия реальной бизнес-логики. «Лишний» код многие инструменты умеют фильтровать и не считать.
Если сомнения в белых пятнах все-таки остаются, возможно посчитать общее покрытие интеграционными тестами и юнит — разработчики наверняка учли многое что труднодоступно для интеграционных тестов.
Однако есть одно «но». Что, если покрытие кода низкое? 20%, 30%? Где-то я читала забавный факт, что покрытие 50% и меньше (по линиям и условиям, как мне помнится) означает тот уровень тестового покрытия, при котором результат работы приложения будет такой же, как и при отсутствии тестирования вообще. Т.е. там могут быть баги, может не быть багов, с тем же успехом вы могли его и не тестировать. Другое объяснение — много мертвого кода, что маловероятно.
А у нас нет автотестов
А они и не нужны. Даже если вас уверяют в обратном, некоторые разработчики не в курсе, что покрытие можно считать не только для юнит тестов. Есть инструменты, которые пишут покрытие в рантайме, т.е. ставите специально обученный инструментированный билд, проходите на нем тесты, а он пишет покрытие.
А смысл?
Моя знакомая прекрасная тест-лид задала вопрос: «когда тест-кейсы есть не все, и автоматизация в зачаточном состоянии, имеет ли смысл тратить ресурсы на оценку покрытия кода?» Внедрение новых штук в процесс всегда вызывает у менеджмента определенную боль: время, ресурсы и прочие бренности существования, никакого простора для полета тестировщика-мечтателя.
Разберем по порядку, куда конкретно нужно будет потратить ресурсы, если вы решите попробовать считать code coverage:
Пункты 1 и 2 можно отдать разработчикам, могие из них знакомы-слышали-встречались с общеизвестными тулами и тем более смогут построить собственный билд. Построение отчетов, как правило, делается одной командой в командной строке или автоматически, если вы используете CI (у меня это делал jenkins, он же публиковал отчет).
Самое затратное — это четвертый пункт. Основная трудность тут в том, что для адекватной оценки надо уметь читать код, либо садиться рядом с разработчиком, чтобы он объяснял, что значит этот кусок, и как это воспроизвести. Это требует определенной квалификации от тест-инженера и рабочего времени 1 или 2 человек.
Стоит ли оно того — решать команде и ее руководителям. В проектах, где требования слабо формализованы, либо баги возникают необъяснимым для тестеров образом, возможно это может помочь хотя бы понять направление куда копать.
Еще одна категория — проекты, которые предполагают очень hight-level black box тестирование. Это прежде всего тестирование через UI или внешний API систем, внутри которых содержится куча логики, работающей по своим законам, т.е. извне вы не можете ее затронуть или ей управлять, а значит не можете нормально протестировать. Анализ покрытия в таких проектах создаст аргументированную необходимость переходить к более «низким» уровням тестирования: модульным, покомпонентным, тестированию на заглушках и т.п.
Хорошо работает накопленное покрытие кода в цифрах: на графиках можно увидеть моменты, когда вливается новый код, а тесты еще не подоспели; если уровень покрытия был высоким, потом стал снижаться, но предыдущего уровня так и не достиг — где-то может быть хорошее белое пятно недошедших до тестирования требований, и т.д.
Пожалуй, это все, что я хотела сказать на сегодня.
Полное покрытие кода
Инструмент тестирования nose
Изначальный пример кода
#!/usr/bin/env python
import operator
Код работает только на Python 2.6 и не совместим с Python 3. Код сохранен в файле main.py.
Юнит-тесты
Начнем с простых тестов:
import unittest
from main import factorial
OK
Добавим еще один класс для стопроцентного покрытия:
class FakeStream :
def readline ( self ):
return ‘5’
Выводы
Адаптация под Python 3
#!/usr/bin/env python
import operator
Теперь программу можно запускать:
$ python3 main.py
Enter the positive number: 0
0! = 1
Значит ли это, что программа рабочая? Нет! Она рабочая только до вызова reduce, что нам и показывают тесты:
$ nosetests3
E. E
======================================================================
ERROR: test_calculation (tests.TestFactorial)
———————————————————————-
Traceback (most recent call last):
File «/home/nuald/workspace/factorial/tests.py», line 9, in test_calculation
self.assertEqual(720, factorial(6))
File «/home/nuald/workspace/factorial/main.py», line 12, in factorial
return reduce(operator.mul, range(1, n + 1))
NameError: global name ‘reduce’ is not defined
FAILED (errors=2)
В данном примере все это можно было обнаружить и ручным тестированием. Однако на больших проектах только юнит-тестирование поможет обнаружить такого рода ошибки. И только полное покрытие кода может гарантировать что практически все несоответствия кода и API были устранены.
Ну и собственно, рабочий код, полностью совместимый между Python 2.6 и Python 3:
#!/usr/bin/env python
import operator
from functools import reduce
import sys
import unittest
from main import factorial
class FakeStream :
def readline ( self ):
return ‘5’
Тестирование
Раздел: Тестирование > Тест дизайн > Тестовое Покрытие
Тестовое Покрытие (Test Coverage)
Если рассматривать тестирование как «проверку соответствия между реальным и ожидаемым поведением программы, осуществляемая на конечном наборе тестов», то именно этот конечный набор тестов и будет определять тестовое покрытие:
Чем выше требуемый уровень тестового покрытия, тем больше тестов будет выбрано, для проверки тестируемых требований или исполняемого кода.
Сложность современного программного обеспечения и инфраструктуры сделало невыполнимой задачу проведения тестирования со 100% тестовым покрытием. Поэтому для разработки набора тестов, обеспечивающего более менее высокий уровень покрытия можно использовать специальные инструменты либо техники тест дизайна.
Существуют следущие подходы к оценке и измерению тестового покрытия:
Ограничения:
Метод оценки покрытия кода не выявит нереализованные требования, так как работает не с конечным продуктом, а с существующим исходным кодом.
Метод покрытия требований может оставить непроверенными некоторые участки кода, потому что не учитывает конечную реализацию.
Покрытие требований (Requirements Coverage)
Расчет тестового покрытия относительно требований проводится по формуле:
Для оптимизации тестового покрытия при тестировании на основании требований, наилучшим способом будет использование стандартных техник тест дизайна. Пример разработки тестовых случаев по имеющимся требованиям рассмотрен в разделе: «Практическое применение техник тест дизайна при разработке тест кейсов»
Покрытие кода (Code Coverage)
Расчет тестового покрытия относительно исполняемого кода программного обеспечения проводится по формуле:
В настоящее время существует инструментарий (например: Clover), позволяющий проанализировать в какие строки были вхождения во время проведения тестирования, благодаря чему можно значительно увеличить покрытие, добавив новые тесты для конкретных случаев, а также избавиться от дублирующих тестов. Проведение такого анализа кода и последующая оптимизация покрытия достаточно легко реализуется в рамках тестирования белого ящика (white-box testing) при модульном, интеграционном и системном тестировании; при тестировании же черного ящика (black-box testing) задача становится довольно дорогостоящей, так как требует много времени и ресурсов на установку, конфигурацию и анализ результатов работы, как со стороны тестировщиков, так и разработчиков.
Тестовое покрытие на базе анализа потока управления
Фундаментом для тестирования потоков управления является построение графов потоков управления (Control Flow Graph), основными блоками которых являются:
Для тестирования потоков управления определены разные уровни тестового покрытия:
Уровень | Название | Краткое описание |
---|---|---|
Уровень 0 | — | “Тестируй все что протестируешь, пользователи протестируют остальное” На английском языке это звучит намного элегантнее: “Test whatever you test, users will test the rest” |
Уровень 1 | Покрытие операторов | Каждый оператор должен быть выполнен как минимум один раз. |
Уровень 2 | Покрытие альтернатив [2] / Покрытие ветвей | Каждый узел с ветвлением (альтернатива) выполнен как минимум один раз. |
Уровень 3 | Покрытие условий | Каждое условие, имеющее TRUE и FALSE на выходе, выполнено как минимум один раз. |
Уровень 4 | Покрытие условий альтернатив | Тестовые случаи создаются для каждого условия и альтернативы |
Уровень 5 | Покрытие множественных условий | Достигается покрытие альтернатив, условий и условий альтернатив (Уровни 2, 3 и 4) |
Уровень 6 | “Покрытие бесконечного числа путей” | Если, в случае зацикливания, количество путей становится бесконечным, допускается существенное их сокращение, ограничивая количество циклов выполнения, для уменьшения числа тестовых случаев. |
Уровень 7 | Покрытие путей | Все пути должны быть проверены |
Таблица 1. Уровни тестового покрытия
Основываясь на данных этой таблицы, вы сможете спланировать необходимый уровень тестового покрытия, а также оценить уже имеющийся.
[1] A practitioner’s Guide to Software Test Design. Lee Copeland
[2] Стандартный глоссарий терминов, используемых в тестировании программного обеспечения Версия 2.0 (от 4 декабря 2008), Подготовлен ‘Glossary Working Party’ International Software Testing Qualifications Board
Автоматический анализ покрытия кода с использованием OpenCover + плюшки
Современные подходы к разработке программного обеспечения делают большой упор на контроль качества. Теперь недостаточно, как раньше, просто писать код, нужно убедиться в том, что этот код правильно написан.
Уже сложно найти проект, в котором отсутствуют юнит-тесты. Их использование многим кажется избыточным, ведь это трата времени, которое с тем же успехом можно потратить на написание другого кода и “не, ну я точно знаю, что там все правильно”. Но, как мы убеждаемся, в долгосрочной перспективе тесты экономят больше времени, чем отнимают. Облегчается сопровождение кода, рефакторинг становится безопасным, отслеживается правильность любых изменений. Причем, чем выше покрытие — тем сильнее чувствуется полезность тестов.
Соответственно, важным моментом является анализ этого самого покрытия, причем желательно построчно, чтобы видеть, какие участки кода не тестируются и иметь возможность быстро исправлять ситуацию.
Проводить подробный анализ покрытия нам помогает инструмент OpenCover. Он работает с кодом на C#. Это замечательное опенсорсное решение, исходники доступны на гитхабе. Документации не особо много, но вполне хватает.
Итак, чтобы начать пользоваться OpenCover, достаточно скачать исходники и собрать, используя Visual Studio. OpenCover являет собой консольное приложение, все необходимые опции задаются параметрами командной строки, так что прикрутить к любому сборщику, будь то MsBuild, Nant, Rake или что либо другое, не проблема.
Интересен механизм работы — OpenCover запускается вместе с прогоном юнит-тестов. Если быть точным, команда на запуск тестов передается ему в качестве нескольких параметров.(Если в аргументе есть пробелы, то он берется в кавычки полностью, например “-target:%application%”):
Благо, сам автор OpenCover подсказывает нам решение — инструмент под названием ReportGenerator. Он опенсорсный, так что качаем исходники, собираем, получаем исполняемый файл и вперед. В использовании ReportGenerator очень прост. Это также консольное приложение, принимающее несколько параметров. Приведу те, которые мы используем, более полную инструкцию можно найти на странице проекта.
Начало положено, покрытие кода анализируется. Дальше — на ваше усмотрение. Я, например, настроил нашу систему Continuous Integration таким образом, чтобы при покрытии ниже требуемого билд падал. Учитывая строгое отношение к завалившимся билдам, неплохо обеспечивает стабильное написание юнит-тестов 🙂