统计 Django 项目的测试覆盖率

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

统计 Django 项目的测试覆盖率

削微寒   2020-03-06 我要评论
![](https://img2018.cnblogs.com/blog/759200/202002/759200-20200213202051843-983322874.jpg) 作者:[HelloGitHub-追梦人物](https://www.zmrenwu.com) > 文中所涉及的示例代码,已同步更新到 [HelloGitHub-Team 仓库](https://github.com/HelloGitHub-Team/HelloDjango-blog-tutorial) 我们完成了对 blog 应用和 comment 应用这两个核心 app 的测试。现在我们想知道的是究竟测试效果怎么样呢?测试充分吗?测试全面吗?还有没有没有测到的地方呢? 单凭肉眼观察难以回答上面的问题,接下来我们就借助 [Coverage.py](https://coverage.readthedocs.io/en/coverage-5.0.3/index.html),从代码覆盖率的角度来检测一下我们的测试效果究竟如何。 [Coverage.py](https://coverage.readthedocs.io/en/coverage-5.0.3/index.html) (以下简称 Coverage)是 Python 测试界最为流行的一个库之一,用来统计测试覆盖率。测试覆盖率可以从一个角度衡量代码的质量,覆盖率越高,说明测试越充分,代码出现 bug 的几率也就越小。当然需要注意的是,测试覆盖率仅仅只是衡量代码质量的一个角度,即使是 100% 的覆盖率也不能说代码就是完美的,没有 bug 的。 ## 安装 Coverage 要使用 Coverage,首先当然是安装它: ```bash $ pipenv install coverage --dev ``` 因为只在开发时才用得到,所以使用 Pipenv 安装时加 --dev 选项将其标记为开发时的依赖库。 ## 简单配置 Coverage Coverage 支持很多配置选项,为了方便,通常将这些配置写在名为 `.coveragerc` 的文件中,Coverage 运行时会从项目根目录读取这个配置文件。因此先在**项目根目录**创建这个文件并写入最基本的配置: ``` [run] branch = True source = . [report] show_missing = True ``` Coverage 的配置遵循 ini 文件语法。简单来说就是,`[section]` 代表一个配置块,用于组织相关的一组配置。例如这里 `[run]` 是一个配置块,`[report]` 是另一个配置块,两个块下都有相关的一些配置项。 配置项的格式为 `key = value` 。 这几个简单配置项的含义为: - `branch = True`。是否统计条件语句的分支覆盖情况。if 条件语句中的判断通常有 True 和 False 两种情况,设置 `branch = True` 后,Coverage 会测量这两种情况是否都被测试到。 - `source = .`。指定需统计的源代码目录,这里设置为当前目录(即项目根目录)。 - `show_missing = True`。在生成的统计报告中显示未被测试覆盖到的代码行号。 ## 运行 Coverage 简单配置后,我们就可以来运行 Coverage 了。 打开命令行,进入项目根目录,依次运行下面的命令(注意如果没有激活虚拟需使用 pipenv run 让命令在虚拟环境中执行)。 首先运行 erase 命令清除上一次的统计信息 ```bash $ pipenv run coverage erase ``` manage.py test 运行 django 单元测试,这是这一次用 coverage run 来运行 ```bash $ pipenv run coverage run manage.py test ``` 生成覆盖率统计报告 ```bash $ pipenv run coverage report ``` 覆盖率统计报告输出如下: ``` Name Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------------------------------------------- _credentials.py 2 2 0 0 0% 1-2 blog\__init__.py 0 0 0 0 100% blog\admin.py 11 0 0 0 100% blog\apps.py 4 0 0 0 100% blog\elasticsearch2_ik_backend.py 8 0 0 0 100% blog\feeds.py 12 0 0 0 100% blog\migrations\0001_initial.py 7 0 0 0 100% blog\migrations\0002_auto_20190711_1802.py 7 0 0 0 100% blog\migrations\0003_auto_20191011_2326.py 4 0 0 0 100% blog\migrations\0004_post_views.py 4 0 0 0 100% blog\migrations\__init__.py 0 0 0 0 100% blog\models.py 62 0 0 0 100% blog\search_indexes.py 8 0 0 0 100% blog\templatetags\__init__.py 0 0 0 0 100% blog\templatetags\blog_extras.py 15 0 0 0 100% blog\tests\__init__.py 0 0 0 0 100% blog\tests\test_models.py 58 0 2 0 100% blog\tests\test_smoke.py 4 0 0 0 100% blog\tests\test_templatetags.py 115 0 2 0 100% blog\tests\test_utils.py 11 0 0 0 100% blog\tests\test_views.py 170 0 8 0 100% blog\urls.py 4 0 0 0 100% blog\utils.py 10 0 2 1 92% 14->16 blog\views.py 40 7 2 0 79% 64-72 blogproject\__init__.py 0 0 0 0 100% blogproject\settings\__init__.py 0 0 0 0 100% blogproject\settings\common.py 22 0 0 0 100% blogproject\settings\local.py 5 0 0 0 100% blogproject\settings\production.py 5 5 0 0 0% 1-8 blogproject\urls.py 4 0 0 0 100% blogproject\wsgi.py 4 4 0 0 0% 10-16 comments\__init__.py 0 0 0 0 100% comments\admin.py 6 0 0 0 100% comments\apps.py 4 0 0 0 100% comments\forms.py 6 0 0 0 100% comments\migrations\0001_initial.py 7 0 0 0 100% comments\migrations\0002_auto_20191011_2326.py 4 0 0 0 100% comments\migrations\__init__.py 0 0 0 0 100% comments\models.py 15 0 0 0 100% comments\templatetags\__init__.py 0 0 0 0 100% comments\templatetags\comments_extras.py 12 0 2 0 100% comments\tests\__init__.py 0 0 0 0 100% comments\tests\base.py 10 0 0 0 100% comments\tests\test_models.py 8 0 0 0 100% comments\tests\test_templatetags.py 57 0 6 0 100% comments\tests\test_views.py 34 0 4 0 100% comments\urls.py 4 0 0 0 100% comments\views.py 17 0 2 0 100% fabfile.py 21 21 0 0 0% 1-43 manage.py 12 2 2 1 79% 11-12, 20->exit scripts\__init__.py 0 0 0 0 100% scripts\fake.py 63 63 14 0 0% 1-106 -------------------------------------------------------------------------------------------- TOTAL 876 104 46 2 87% ``` 倒数第二列是被统计文件的测试覆盖率,第一列是未被覆盖的代码行号。 大部分文件测试覆盖率为 100%,说明我们的测试还是比较充分的。但从报告结果中我们发现这样几个问题: 1. 有一些文件其实并不需要测试,或者并非项目的核心文件(例如部署脚本 fabfile.py,django 的 migrations 文件等),这些文件应该从统计中排除。 2. Coverage 默认显示全部文件的覆盖率统计结果,如果文件比较多的话就不好查找非 100% 覆盖率的文件。毕竟我们的目标是提高代码覆盖率,因此已达 100% 覆盖的代码文件我们不再关心。我们要做的是找到非 100% 覆盖率的文件,为其添加缺失的测试。 ## 完善 Coverage 配置 可以通过添加 Coverage 配置项轻松解决上面 2 个问题。 在 `[run]` 配置块中增加 `omit` 配置项可以指定排除统计的文件。 在 `[report]` 配置块中增加 `skip_covered` 配置项可以指定统计报告中不显示 100% 覆盖的文件。 这是 `.coveragerc` 最终配置结果,注意我们在 omit 配置项中指定忽略了一些非核心的项目文件: ``` [run] branch = True source = . omit = _credentials.py manage.py blogproject/settings/* fabfile.py scripts/fake.py */migrations/* blogproject\wsgi.py [report] show_missing = True skip_covered = True ``` 再次按照上一节所说的方式运行 Coverage,最终报告结果如下: ``` Name Stmts Miss Branch BrPart Cover Missing ----------------------------------------------------------- blog\utils.py 10 0 2 1 92% 14->16 blog\views.py 40 7 2 0 79% 64-72 ----------------------------------------------------------- TOTAL 709 7 30 1 99% 33 files skipped due to complete coverage. ``` 这个报告指出我们仍有 2 个文件没有达到 100% 的覆盖率,我们要做的就是为这两个文件中未测试的代码增加单元测试,让其达到 100% 测试覆盖率。 不过在动手写测试之前,我们要搞清楚哪些代码没被测到。命令行报告的最后一列指出了未被测试代码的行号,但是这样看着不是很直观。一种体验更好的方式是生成 HTML 报告,这样我们可以直接在 HTML 报告中查看到未被测试到的具体代码。 ## 生成 HTML 报告 `coverage report` 命令在命令行生成统计报告,而 `coverage html` 则可以生成 HTML 报告。 在上一节的基础上,运行如下命令: ```bash $ pipenv run coverage html ``` 运行完成后项目根目录会多出一个 htmlcov 的文件夹,里面就是测试覆盖率的 HTML 报告文件。用浏览器打开里面的 index.html 文件就可以查看报告结果了: ![](https://img2020.cnblogs.com/blog/759200/202003/759200-20200305135059863-1454372220.png) 主页和命令行的结果是一样的,不过我们可以点击文件名,进入到对这个文件更加具体的统计报告页面,例如 **blog\views.py** 结果如下: ![](https://img2020.cnblogs.com/blog/759200/202003/759200-20200305135117451-702760735.png) 绿色部分代表已覆盖的代码,红色部分代表为覆盖的代码。 ## 完善单元测试 查看文件我们发现,**blog\views.py** 中未被覆盖的代码原来是 [Django 博客实现简单的全文搜索](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/85/) 中的代码,现在我们已经将搜索替换为 [Django Haystack 全文检索](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/86/) 了,这段代码也就不需要了,可以直接删除。 **blog\views.py** 的报告结果则表明我们在 [Django Haystack 全文检索与关键词高亮](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/86/) 中自定义的搜索关键词高亮器有一个 if 分支条件未被测试到: ![](https://img2020.cnblogs.com/blog/759200/202003/759200-20200305135125866-1113873626.png) 检查 blog/tests/test_utils.py 中的测试用例,我们发现只测试了比较短的标题不被截断,也就是 ```python if len(text_block) < self.max_length: ``` 判断条件为 True,缺失对判断条件为 False 的测试。所以我们来构造一个新的测试用例测试标题长度超过 `max_length` (默认值为 200)的情况时会被截断: ```python class HighlighterTestCase(TestCase): def test_highlight(self): # 省略已有代码 ... highlighter = Highlighter("标题") document = "这是一个长度超过 200 的标题,应该被截断。" + "HelloDjangoTutorial" * 200 self.assertTrue( highlighter.highlight(document).startswith( '...标题,应该被截断。' ) ) ``` 再次运行 Coverage 生成报告,测试覆盖率全都 100% 了! ```bash $ pipenv run coverage erase $ pipenv run coverage run manage.py test $ pipenv run coverage report # 输出 Name Stmts Miss Branch BrPart Cover Missing --------------------------------------------------- --------------------------------------------------- TOTAL 704 0 28 0 100% ``` 最后提醒一点,Coverage 运行后可能会在项目目录下生成一些文件,这些文件并不需要纳入版本管理,所以将其加入 .gitignore 文件中,防止被提交到代码库: ``` htmlcov/ .coverage .coverage.* coverage.xml *.cover ``` ## HelloDjango 往期回顾: 第 30 篇:[Django 博客单元测试:测试评论应用](https:////www.cnblogs.com/xueweihan/p/12372401.html) 第 29 篇:[编写 Django 应用单元测试](https:////www.cnblogs.com/xueweihan/p/12336462.html) 第 28 篇:[Django Haystack 全文检索与关键词高亮](https://www.cnblogs.com/xueweihan/p/12304983.html) --- ![](https://img2018.cnblogs.com/blog/759200/202002/759200-20200213201956024-782757549.png) **关注公众号加入交流群**

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们