Python单元测试:unit test code coverage

By | 2019年2月11日

Python单元测试:unit testing best practices
Python单元测试:unit test code coverage

开发环境配置

单元测试覆盖率

  • unittest: Python built-in standard library
  • Pytest: a third party Python testing framework
  • Coverage.py:one of the most popular code coverage tools for Python
  • Pytest-cov:Python plugin to generate coverage reports. In addition to functionalities supported by coverage command, it also supports centralized and distributed testing
  • Nose可以将所有的单元测试文件一次全部执行,并且提供了coverage的插件,能够统计整体的覆盖率,官网有详细的使用介绍https://pypi.org/project/nose/
bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp$ pwd
/c/Users/bobho/Desktop/tmp

bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp$ dir
  app.py  test.py

bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp$ cat app.py
#!/usr/bin/env python

# -*- coding: utf-8 -*-

import sys

def process_input(a, b, operation):
    if operation == "add":
        return a + b
    if operation == "subtract":
        return a - b
    if operation == "multiple":
        return a * b
    if operation == "divide":
        if b == 0:
            return "Invalid input"
        return a / b

if __name__ == "__main__":
        print(process_input(int(sys.argv[1]), int(sys.argv[2]), sys.argv[3]))
bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp
$ cat app.py
#!/usr/bin/env python

# -*- coding: utf-8 -*-

import sys

def process_input(a, b, operation):
    if operation == "add":
        return a + b
    if operation == "subtract":
        return a - b
    if operation == "multiple":
        return a * b
    if operation == "divide":
        if b == 0:
            return "Invalid input"
        return a / b

if __name__ == "__main__":
        print(process_input(int(sys.argv[1]), int(sys.argv[2]), sys.argv[3]))
bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp

bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp
$ python test.py
test_0010_add (__main__.TestApp) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp
$ pip3 install coverage
Collecting coverage
  Downloading https://files.pythonhosted.org/packages/9c/c2/036ccdc0bbcef7d980dd                                                                                                                                  f89c132b39c81b2ba645f1c904b0c8edc957f60a/coverage-4.5.2-cp36-cp36m-win_amd64.whl                                                                                                                                   (183kB)
Installing collected packages: coverage
Successfully installed coverage-4.5.2
You are using pip version 9.0.3, however version 18.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' comm                                                                                                                                  and.

bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp
$ coverage run test.py
test_0010_add (__main__.TestApp) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp
$ coverage report
Name      Stmts   Miss  Cover
-----------------------------
app.py       14      9    36%
test.py      15      0   100%
-----------------------------
TOTAL        29      9    69%


添加单元测试,更新后的test.py如下

import unittest

from app import process_input

class TestApp(unittest.TestCase):
    def setUp(self):
        self.a = 10
        self.b = 5

    def test_0010_add(self):
        result = process_input(self.a, self.b, "add")
        self.assertEqual(result, 15)

    def test_0020_subtract(self):
        result = process_input(self.a, self.b, "subtract")
        self.assertEqual(result, 5)


def suite():
    suite = unittest.TestSuite()
    suite.addTests(
        unittest.TestLoader().loadTestsFromTestCase(TestApp)
    )

    return suite


if __name__ == "__main__":
    unittest.TextTestRunner(verbosity=2).run(suite())

再次运行覆盖率检查,可以看到覆盖率已经提升

$ coverage run test.py
test_0010_add (__main__.TestApp) ... ok
test_0020_subtract (__main__.TestApp) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

bobho@LAPTOP-ET2KSKHR MINGW64 ~/Desktop/tmp
$ coverage report
Name      Stmts   Miss  Cover
-----------------------------
app.py       14      7    50%
test.py      18      0   100%
-----------------------------
TOTAL        32      7    78%

运行coverage html会在当前目录下面生成一个htmlcov,打开其中的app_py.html可以看到具体哪些行被覆盖到了

13918377-19b7f2b31bf00b1e.png
image.png

默认情况下,coverage生成的结果文件为.coverage,你可以通过修改环境变量COVERAGE_FILE来修改这个文件的后缀名。你也可以是用-a把多次运行的结果合并到一个文件里,否则,每次生成的结果文件都是上一次运行的结果。你可以是用coverage erase清空之前运行的结果文件。

coverage threshold

covearge可以使用参数–fail-under指定一个数字,当coverage的结果小于这个数字,coverage命令返回一个错误码2,但这个参数对annotate命令无效

$ coverage run run_my_tests.py 
... running all tests
$ coverage report --fail-under=100
... display the report
$ echo $?
2

如果覆盖率不是100%,则返回值是2。也可以通过python api来验证覆盖率,示例如下:

cov = coverage.coverage(..)
cov.start()
ret = run_all_my_tests()
cov.stop()
if ret == 0:
    covered = cov.report()
    assert covered > 100, "Not enough coverage"
unittest框架

unittest 框架默认根据ASCII码的顺序加载测试用例,数字与字母的顺序为:09,AZ,a~z 如果要让某个测试用例先执行,不能使用默认的main()方法,需要通过TestSuite类的addTest()方法按照一定的顺序来加载

如果需要,用户可以自己实现discover(),遍历目录并添加和组合测试用例。利用discover获取指定目录或多级目录下的测试用例,但存放用例的目录属性必须是python package,必须要有init.py,不然不会获取成功

发表评论