Как измерить покрытие автотестами? Данный вопрос всегда вызывает жаркие дискуссии в рядах тестировщиков. Некоторые меряют покрытие по требованиям, на сколько пунктов были написаны автотесты, такое покрытие и есть. Другие по задачам в бэклоге, где находятся нереализованные тесты и баги, которые требуют покрытия. В общем случае, измерять покрытие становится сложнее, когда мы движемся вверх по пирамиде тестирования и уровень наших тестов растет. Как здорово на уровне Unit тестов, там отлично показывается сколько строк кода покрыто, процент и т.д. Вот если бы подобный отчет можно было сделать для end-to-end тестов или UI, то сразу было бы понятно реальное тестовое покрытие и каких тестов не хватает. К счастью, есть такой инструмент, который позволяет строить подробный отчет по покрытию Python сервисов - Coverage.py.
Существуют два подхода к измерению тестового покрытия сервисов. Первый, когда утилита собирает тестируемый код в специальный образ, где расставляет контрольные маркеры прям в коде на строчках, функциях, методах и т.д. Тем самым, образ тестируемого сервиса становится больше. После прогона автотестов, собирается отчет на основе пройденных маркеров и сравнения с исходным кодом. Пример такой утилиты - это Сobertura, которая используется для Java.
Второй подход - это снятие покрытия кода в реальном времени. Утилита подключается к виртуальной машине или интерпретатору и регистрирует вывозы различных операций. После, сравнивает результат с исходным кодом и строит свой отчет. Такой подход используют утилиты, например, JaCoCo для Java и Coverage.py для Python. Отличия этих утилит друг от друга в том, что JaCoCo подключается к Java машине, а в основе Covarage.py лежит функция трассировки. Это функция, которую интерпретатор Python вызывает для каждой строки, выполняемой в программе. Covarage.py запускает проверяемый код в своем “врапере”, где выполняет функцию трассировки, которая записывает каждый файл и номер строки по мере его выполнения.
Посмотрим на примере, как работает Covarege.py. Есть простой API сервис. В котором 3 endpoint-а. Как видно, в 3-ем endpoint-е '/second/{branch}' идет параметризированный вывод. Параметр branch обязателен в этом запросе, поэтому код никогда не зайдет в ветку с сообщением 'Unreachable'
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def index_page():
return {'message': 'Hello habr'}
@app.get('/first')
async def first_page():
return {'message': 'The first page'}
@app.get('/second/{branch}')
async def second_page(branch):
msg_type = 'Reachable'
if not branch:
msg_type = 'Unreachable'
return {'message': f'{msg_type} response'}
Запускаем сервис во “врапере” Covarage.py. Дальше, открываем браузер и переходим по всем трем endpoint-ам.
coverage run -a manage.py start-test-app
После завершения “тестирования”, нужно остановить сервис, для того, чтобы Covarage.py сформировал файл .coverage, который является sqlite базой. Для представления отчета в удобном виде, сконвертируем его в html и откроем в браузере.
coverage html
Отчет показывает 96% по всем файлами и 93% по файлу с кейсом, где одна ветка кода недостижима. Если мы “провалимся” в отчет по файлу main.py, то увидим, что данная строчка красная, но само условие зеленое.
Covarage.py в данном режиме считает statement. Как результат, условие получилось пройденным и красным подсвечена только невыполненная строка. В простом коде не составляет труда понять какие условия нужно выполнить или поправить, чтобы добраться до непокрытых строк. Но в промышленном коде, такие моменты могут быть неочевидны и создадут дополнительные трудности при анализе отчета.
Тем более, тестировщикам более важно не количество покрытых строк, а условий и различных веток выполнения кода. В Covarage.py есть специальный аргумент, для подсчета веток затронутого кода. Запустим тестовое приложение с этим аргументом и повторим действия.
coverage run --branch -a manage.py start-test-app
В итоге, Covarage.py сформировал отчет относительно ветвей исполнения кода. Как видно, покрытие упало до 90% по всем файлам и до 88% по файлу с кейсом. Появились две дополнительных колонки:
Branch - означает количество ветвлений обнаруженных в результате прогона;
Partial - показывает количество ветвлений, где не сработало условие.
Заглянем в main.py. Видно, что строчка с ветвлением подсвечена желтым, а это значит, что это условие не выполнилось и как результат, данная ветка кода не отработала.
Неотработавшие условия - это повод перепроверить свои тестовые кейсы на полноту. Также, это позволяет найти неиспользованный, legacy, код, который хорошо бы удалить. Рекомендую, разбирать данный отчет вместе с разработчиками, так как только они могут понять насколько этот код в принципе достижим и подсказать условия его достижения. В результате анализа, разработчик может предложить исключить часть файлов из отчета, так как это служебные файлы, конфиги и т.д. Тогда необходимо составить список таких файлов и директорий и исключить их из отчета.
Covarage.py - это отличный инструмент для измерения покрытия кода тестами. Нужно быть аккуратней с анализом отчета, выбирать нужный режим при его генерации и исключить ненужные файлы. Качество продукта - это ответственность всей команды, не только тестировщиков. Поэтому привлекайте разработчиков к тестированию, настройке окружения и анализу отчета.