如何在Django + Nose中正确测试覆盖率
目前我有一个项目,配置了通过Django的管理命令来运行代码覆盖率,具体是这样做的:
./manage.py test --with-coverage --cover-package=notify --cover-branches --cover-inclusive --cover-erase
这样会生成一个类似下面的报告:
Name Stmts Miss Branch BrMiss Cover Missing
--------------------------------------------------------------------------
notify.decorators 4 1 0 0 75% 4
notify.handlers 6 1 2 0 88% 11
notify.notification_types 46 39 2 0 19% 8-55, 59, 62, 66
notify.notifications 51 51 0 0 0% 11-141
--------------------------------------------------------------------------
TOTAL 107 92 4 0 17%
不过,这个报告有个问题。它是错误的。覆盖率显示某些行没有被测试覆盖,尽管实际上它们是被测试覆盖的。例如,如果我用nosetests
来运行测试,而不是用Django的管理命令,我得到的报告是正确的:
Name Stmts Miss Branch BrMiss Cover Missing
-----------------------------------------------------------------------------
notify.decorators 4 0 0 0 100%
notify.handlers 6 0 2 0 100%
notify.notification_types 46 0 2 0 100%
notify.notifications 51 25 0 0 51% 13, 18, 23, 28, 33, 38, 43, 48, 53, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 116, 121, 126, 131, 136, 141
-----------------------------------------------------------------------------
TOTAL 107 25 4 0 77%
我在谷歌上找到了覆盖率网站的常见问题解答,链接是 http://nedbatchelder.com/code/coverage/faq.html
问:为什么函数(或类)的主体显示为已执行,但定义行却没有?
这是因为覆盖率是在函数定义之后才开始的。定义行在没有覆盖率测量的情况下被执行,然后才开始覆盖率测量,接着才调用函数。这意味着函数的主体被测量了,但函数本身的定义却没有。
要解决这个问题,需要更早地启动覆盖率。如果你使用命令行来运行程序并监控覆盖率,那么整个程序都会被监控。如果你使用API,则需要在导入定义函数的模块之前调用coverage.start()。
我的问题是,我能否通过Django的管理命令正确运行覆盖率报告?还是说我必须绕过管理命令,以避免在“缺失”的行被执行后才启动覆盖率的情况?
5 个回答
我之前也遇到过类似的问题,使用远程解释器在虚拟机里通过ssh配置。解决办法是把我的测试文件夹和所有上级文件夹都设置到“运行” > “编辑配置...”中的“环境”部分的“路径映射”里。
我已经成功让这个工作起来,包括在我的 manage.py 文件上方加了一些东西。
import coverage
我其实是在用 Flask,但遇到的这个问题和我用的工具是一样的。
我的问题是,这在控制台里能正常工作,但 Jenkins 却不知道这一点,老是说那些导入的东西不在测试范围内……
有没有什么想法?
我花了一些时间在这个问题上,尽管有一些答案,但它们并没有详细解释我遇到的情况。现在对我来说,以下方法效果很好,参考了iyn的答案,并做了一些必要的调整。我的manage.py文件看起来是这样的:
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
# See https://stackoverflow.com/questions/24668174/how-to-test-coverage-properly-with-django-nose
is_coverage_testing = 'test' in sys.argv and '--with-coverage' in sys.argv
# Drop dupe with coverage arg
if '--with-coverage' in sys.argv:
sys.argv.remove('--with-coverage')
if is_coverage_testing:
import coverage
cov = coverage.coverage(source=['client_app', 'config_app', 'list_app', 'core_app', 'feed_app',
'content_app', 'lib',
'job_app', 'license_app', 'search_app', 'weather_app'],
omit=['*/integration_tests/*'])
cov.erase()
cov.start()
execute_from_command_line(sys.argv)
if is_coverage_testing:
cov.stop()
cov.save()
cov.report()
从上面可以看到,我把所有的应用程序都包括在测试中,但把我存放集成测试的地方排除了。
在我的settings.py
文件中,我不再使用cover包和with-coverage
,因为这些现在已经在manage.py
中处理了。以下是我的设置以及一些解释:
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# These are global options, trim as needed
# See https://stackoverflow.com/questions/24668174/how-to-test-coverage-properly-with-django-nose
NOSE_ARGS = [
# '--cover-package=client_app', # included in manage.py (hack to include all app testing)
# '--cover-package=config_app',
# '--cover-package=content_app',
# '--cover-package=job_app',
# '--cover-package=lib',
# '--cover-package=license_app',
# '--cover-package=list_app',
# '--cover-package=search_app',
# '--cover-package=core_app',
# '--cover-package=weather_app',
# '--cover-package=feed_app',
'--logging-level=INFO',
'--cover-erase',
# '--with-coverage', # Included in manage.py (hack), do not use here or will create multiple reports
# '--cover-branches', # Lowers coverage
'--cover-html', # generate HTML coverage report
'--cover-min-percentage=59',
# '--cover-inclusive', # can't get coverage results on most files without this... This breaks django tests.
]
我这样运行我的基本测试(带覆盖率):
./manage.py test --noinput --verbose --with-coverage
现在我可以看到models.py、admins.py和apps.py都被覆盖了。
我这样运行我的集成测试(不带覆盖率):
./manage.py test integration_tests/itest_* --noinput
我也可以这样运行一组特定的测试:
./manage.py test --noinput --verbose client_app/tests.py
你可以根据需要修改NOSE_ARGS
,或者如果你打算每次在命令行上使用标志,可以完全不写它。祝好运!
根据文档的说明,“使用命令行来运行你的程序并查看覆盖率”:
coverage run --branch --source=notify ./manage.py test
目前,无法准确地在使用django-nose的情况下运行代码覆盖率统计,因为Django 1.7加载模型的方式有些问题。所以,如果你想获取覆盖率统计数据,需要直接在命令行中使用coverage.py,比如:
$ coverage run --branch --source=app1,app2 ./manage.py test
$ coverage report
$ coverage html -d coverage-report
你可以把coverage.py的设置放在项目根目录下的.coveragerc文件里(也就是和manage.py在同一个文件夹)。
这个问题已经在django-nose的GitHub页面上被报告过:https://github.com/django-nose/django-nose/issues/180,所以维护者们知道这个问题。如果你也遇到这个问题,可以告诉他们。
更新
eliangcs在GitHub上提到,解决这个问题的一个方法是修改你的manage.py
:
import os
import sys
if __name__ == "__main__":
# ...
from django.core.management import execute_from_command_line
is_testing = 'test' in sys.argv
if is_testing:
import coverage
cov = coverage.coverage(source=['package1', 'package2'], omit=['*/tests/*'])
cov.erase()
cov.start()
execute_from_command_line(sys.argv)
if is_testing:
cov.stop()
cov.save()
cov.report()
这样做是有效的,但这个方法有点“黑科技”。
更新 2
我建议所有使用nose的人看看py.test(http://pytest.org/),这是一个非常好的Python测试工具,和Django兼容得很好,还有很多插件等等。我之前用的是django-nose,但试过py.test后就再也没有回头过。