zoukankan      html  css  js  c++  java
  • 统计 Django 项目的测试覆盖率

    作者:HelloGitHub-追梦人物

    文中所涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库

    我们完成了对 blog 应用和 comment 应用这两个核心 app 的测试。现在我们想知道的是究竟测试效果怎么样呢?测试充分吗?测试全面吗?还有没有没有测到的地方呢?

    单凭肉眼观察难以回答上面的问题,接下来我们就借助 Coverage.py,从代码覆盖率的角度来检测一下我们的测试效果究竟如何。

    Coverage.py (以下简称 Coverage)是 Python 测试界最为流行的一个库之一,用来统计测试覆盖率。测试覆盖率可以从一个角度衡量代码的质量,覆盖率越高,说明测试越充分,代码出现 bug 的几率也就越小。当然需要注意的是,测试覆盖率仅仅只是衡量代码质量的一个角度,即使是 100% 的覆盖率也不能说代码就是完美的,没有 bug 的。

    安装 Coverage

    要使用 Coverage,首先当然是安装它:

    $ 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 命令清除上一次的统计信息

    $ pipenv run coverage erase
    

    manage.py test 运行 django 单元测试,这是这一次用 coverage run 来运行

    $ pipenv run coverage run manage.py test
    

    生成覆盖率统计报告

    $ 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%
    blogadmin.py                                       11      0      0      0   100%
    blogapps.py                                         4      0      0      0   100%
    blogelasticsearch2_ik_backend.py                    8      0      0      0   100%
    blogfeeds.py                                       12      0      0      0   100%
    blogmigrations001_initial.py                      7      0      0      0   100%
    blogmigrations002_auto_20190711_1802.py           7      0      0      0   100%
    blogmigrations003_auto_20191011_2326.py           4      0      0      0   100%
    blogmigrations004_post_views.py                   4      0      0      0   100%
    blogmigrations\__init__.py                          0      0      0      0   100%
    blogmodels.py                                      62      0      0      0   100%
    blogsearch_indexes.py                               8      0      0      0   100%
    blog	emplatetags\__init__.py                        0      0      0      0   100%
    blog	emplatetagslog_extras.py                    15      0      0      0   100%
    blog	ests\__init__.py                               0      0      0      0   100%
    blog	ests	est_models.py                           58      0      2      0   100%
    blog	ests	est_smoke.py                             4      0      0      0   100%
    blog	ests	est_templatetags.py                    115      0      2      0   100%
    blog	ests	est_utils.py                            11      0      0      0   100%
    blog	ests	est_views.py                           170      0      8      0   100%
    blogurls.py                                         4      0      0      0   100%
    blogutils.py                                       10      0      2      1    92%   14->16
    blogviews.py                                       40      7      2      0    79%   64-72
    blogproject\__init__.py                              0      0      0      0   100%
    blogprojectsettings\__init__.py                     0      0      0      0   100%
    blogprojectsettingscommon.py                      22      0      0      0   100%
    blogprojectsettingslocal.py                        5      0      0      0   100%
    blogprojectsettingsproduction.py                   5      5      0      0     0%   1-8
    blogprojecturls.py                                  4      0      0      0   100%
    blogprojectwsgi.py                                  4      4      0      0     0%   10-16
    comments\__init__.py                                 0      0      0      0   100%
    commentsadmin.py                                    6      0      0      0   100%
    commentsapps.py                                     4      0      0      0   100%
    commentsforms.py                                    6      0      0      0   100%
    commentsmigrations001_initial.py                  7      0      0      0   100%
    commentsmigrations002_auto_20191011_2326.py       4      0      0      0   100%
    commentsmigrations\__init__.py                      0      0      0      0   100%
    commentsmodels.py                                  15      0      0      0   100%
    comments	emplatetags\__init__.py                    0      0      0      0   100%
    comments	emplatetagscomments_extras.py            12      0      2      0   100%
    comments	ests\__init__.py                           0      0      0      0   100%
    comments	estsase.py                              10      0      0      0   100%
    comments	ests	est_models.py                        8      0      0      0   100%
    comments	ests	est_templatetags.py                 57      0      6      0   100%
    comments	ests	est_views.py                        34      0      4      0   100%
    commentsurls.py                                     4      0      0      0   100%
    commentsviews.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%
    scriptsfake.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/*
       blogprojectwsgi.py
    
    [report]
    show_missing = True
    skip_covered = True
    

    再次按照上一节所说的方式运行 Coverage,最终报告结果如下:

    Name            Stmts   Miss Branch BrPart  Cover   Missing
    -----------------------------------------------------------
    blogutils.py      10      0      2      1    92%   14->16
    blogviews.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 报告。

    在上一节的基础上,运行如下命令:

    $ pipenv run coverage html
    

    运行完成后项目根目录会多出一个 htmlcov 的文件夹,里面就是测试覆盖率的 HTML 报告文件。用浏览器打开里面的 index.html 文件就可以查看报告结果了:

    主页和命令行的结果是一样的,不过我们可以点击文件名,进入到对这个文件更加具体的统计报告页面,例如 blogviews.py 结果如下:

    绿色部分代表已覆盖的代码,红色部分代表为覆盖的代码。

    完善单元测试

    查看文件我们发现,blogviews.py 中未被覆盖的代码原来是 Django 博客实现简单的全文搜索 中的代码,现在我们已经将搜索替换为 Django Haystack 全文检索 了,这段代码也就不需要了,可以直接删除。

    blogviews.py 的报告结果则表明我们在 Django Haystack 全文检索与关键词高亮 中自定义的搜索关键词高亮器有一个 if 分支条件未被测试到:

    检查 blog/tests/test_utils.py 中的测试用例,我们发现只测试了比较短的标题不被截断,也就是

    if len(text_block) < self.max_length:
    

    判断条件为 True,缺失对判断条件为 False 的测试。所以我们来构造一个新的测试用例测试标题长度超过 max_length (默认值为 200)的情况时会被截断:

    class HighlighterTestCase(TestCase):
        def test_highlight(self):
            # 省略已有代码 ...
    
            highlighter = Highlighter("标题")
            document = "这是一个长度超过 200 的标题,应该被截断。" + "HelloDjangoTutorial" * 200
            self.assertTrue(
                highlighter.highlight(document).startswith(
                    '...<span class="highlighted">标题</span>,应该被截断。'
                )
            )
    

    再次运行 Coverage 生成报告,测试覆盖率全都 100% 了!

    $ 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 博客单元测试:测试评论应用

    第 29 篇:编写 Django 应用单元测试

    第 28 篇:Django Haystack 全文检索与关键词高亮


    关注公众号加入交流群

  • 相关阅读:
    springboot redis多数据源
    springboot mybatis 多数据源配置
    java jackson 忽略不存在的属性字段 和 按照属性名转json
    java 四舍五入保留两位小数
    supervisord supervisorctl 问题supervisor.sock refused connection
    2
    1
    Python开发【程序】:三级菜单程序
    Python开发【第二章】:补充
    Python开发【第二章】:深浅拷贝剖析
  • 原文地址:https://www.cnblogs.com/xueweihan/p/12419866.html
Copyright © 2011-2022 走看看