В рамках программы по улучшению качества кода начали писать на работе модульные тесты.
Они оказались достаточно полезными. Пока, конечно рано, говорить насколько увеличилось качество и предсказуемость кода. Но теперь я не боюсь делать рефакторинг, даже переписать целый модуль. Тесты дают мне уверенность, что я ничего не сломал :).
В этой уверенности кроется одна проблема, я не знаю какая часть кода покрыта тестами. Так как нам еще далеко до TDD, тесты проверяют лишь некоторые функции.
Если бы степень покрытия различных модулей системы была известна, мы бы могли ответить на два вопроса:
- С какой вероятностью рефакторинг не добавил ошибок.
- Над тестами к каким модулям стоит поработать для более равномерного покрытия.
Сказано — сделано. Для Python есть замечательная библиотека coverage, которая умеет определять степень покрытия кода.
Библиотеки такого рода работают достаточно просто, в исходных файлах подсчитывается количество строк кода. Затем библиотека запускает хук, который отслеживает, какие строчки кода были выполнены. В итоге степень покрытия это число выполненных во время тестов строк кода, поделенное на общее число строк кода.
Итак, coverage умеет высчитывать степень покрытия кода и строить изумительные html-отчеты, осталось немного — подружить ее с Django.
Для себя я выбрал следующий вариант. Обертка для стандратного test runner, которая по окончании работы генерирует html-отчет. На мой взгляд, достаточно удобно. Одна команда:
./manage.py test
и после запуска тестов у нас уже есть красивые отчеты, которые можно показать коллегам или начальству
Перейдем к самому интересному, к коду обертки, она требует совсем немного настроек в settings.py
#переопределиние стандартного test runner на наш
TEST_RUNNER = "core.test.coverage_runner.run_tests"
#путь к сгенерированным отчетам
COVERAGE_REPORT_PATH = os.path.join(workdir, 'coverage_report')
и кода в файле coverage_runner.py
"""
Test runner with code coverage.
Falls back to django simple runner if coverage-lib not installed
"""
import os
from django.test import simple
from django.conf import settings
#попытка импорта coverage библиотеки
try:
from coverage import coverage as Coverage
except ImportError:
#если она не установлена, просто запустим стандартный обработчик
run_tests = simple.run_tests
else:
#если установлена примемся за построение отчета
coverage = Coverage()
def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]):
# часть непосредственно отвечающая за получение данных о покрытии (включить сбор; выполнить тесты; выключить сбор)
coverage.start()
test_results = simple.run_tests(test_labels, verbosity, interactive, extra_tests)
coverage.stop()
# Как известно, manage.py test может не все тесты, а только из определенных приложений
# немного не логично, что в этом случае покрытие будет определено для всего кода в проекте
# и для не ваших приложений и для библиотек.
# Следующий код берет имена приложений для тестирования из test_labels, определяет все модули
# входящие в их состав и передает список для построения отчета.
# Это очень удобно, мы тестируем только свой код и получаем только его покрытие.
coverage_modules = []
for app in test_labels:
try:
module = __import__(app, globals(), locals(), [''])
except ImportError:
# Эта ситуация возникает в случае если тестирование ограничено одним TestCase, либо модуль недоступен
# В обоих случая нам не нужен отчет о покрытии.
coverage_modules = None
break
if module:
base_path = os.path.join(os.path.split(module.__file__)[0], "")
# Ищем внутри приложения непустые .py файлы
for root, dirs, files in os.walk(base_path):
for fname in files:
if fname.endswith(".py") and os.path.getsize(os.path.join(root, fname)) > 1:
try:
mname = os.path.join(app, os.path.join(root, fname).replace(base_path, ""))
coverage_modules.append(mname)
except ImportError:
pass #do nothing
# Строим html-отчет
if coverage_modules or not test_labels:
coverage.html_report(coverage_modules, directory=settings.COVERAGE_REPORT_PATH)
return test_results
Для успешного использования coverage runner необходимо:
На выходе получаются красивые картинки. Общий отчет
, отчет покрытия одного файла
.
С радостью выслушаю замечания, мысли и истории "как у нас" по модульному тестированию и TDD в комментариях.
Еще предлагаю померяться в комментариях степенью покрытия кода. У нас — 49%.