Python单元测试

1. PyTest安装:

1
2
3
4
5
>>> pip install pytest

# 常用插件安装
>>> pip install pytest-mock # mocker插件
>>> pip install pytest-cov # 单测覆盖率插件

2. 基础用法

定义一个函数如下:

1
2
3
def get_sum(a, b):
print "calling get_sum function"
return a + b

为了验证其功能,我们可以编写单测用例如下:

1
2
3
4
5
6
7
import pytest

class TestTmpFunction(object):
def test_sum(self):
result = get_sum(1, 2)
print result
assert result == 3

运行用例:

1
>>> python -m pytest -v test_tmp.py -s

2.1 命令行参数

可以通过pytest -help 查看支持的参数。以下是一些常用的参数:

  • -v: 输出更详细的用例执行信息, 不使用 -v 参数,运行时不会显示运行的具体测试用例名称;
  • -s: 显示print内容 在运行测试用例时,为了调试或打印一些内容,我们会在代码中加一些print内容,但是这些内容默认不会显示出来。如果带上-s,就可以显示了。
  • -x: 出现一条测试用例失败就退出测试。
  • -m: 用表达式指定多个标记名。 pytest 提供了一个装饰器 @pytest.mark.xxx,用于标记测试并分组,以便你快速选中并运行,各个分组直接用 and、or 来分割。

2.2 选择执行的测试用例(静态)

按文件夹执行

1
2
# 执行指定文件夹及子文件夹下的所有测试用例
pytest ../tests

按文件执行

1
2
# 运行test_tmp.py下的所有的测试用例
pytest test_tmp.py

按测试类执行

1
2
# pytest 文件名.py::测试类
pytest test_tmp.py::TestTmp

按测试方法执行

1
2
# pytest 文件名.py::测试类::测试方法
pytest test_tmp.py::TestTmpFunction::test_sum

选择执行的测试用例(动态)

如要使用动态指定测试用例的方式,首先需要给测试用例打标签(mark),比如在 classmethod 上加上如下装饰器:

1
@pytest.mark.dev_test

在运行时,可以根据标签来动态的选择哪些用例需要执行

1
2
3
4
5
6
7
8
# 同时选中带有这两个标签的所有测试用例运行
pytest -m "mark1 and mark2"

# 选中带有mark1的测试用例,不运行mark2的测试用例
pytest -m "mark1 and not mark2"

# 选中带有mark1或 mark2标签的所有测试用例
pytest -m "mark1 or mark2"

除此之外还提供了一种通过模糊匹配的方式选择测试用例的方式:

1
2
# -k 参数是按照文件名、类名、方法名、标签名来模糊匹配的
pytest -k xxxPattern

3. mock使用

pytest自带的unittest框架中默认集成了mock库,PyTest的mock支持是通过插件实现的。相对来讲PyTest使用起来更简单(PyTest的mocker是对原生mock的一个兼容,原生mock支持的功能mocker基本都可以支持)

3.1 基础用法

1
2
3
4
5
6
7
8
9
10
11
def get_sum(a, b):
print "calling get_sum function"
return a + b


class TestTmpFunction(object):
def test_sum_with_mock(self, mocker):
mocker.patch('test_tmp.get_sum', return_value=3)
result = get_sum(1, 2)
print result
assert result == 3

运行后可以发现,原本get_sum的print内容并没有被打印出来,我们通过mocker.patch方法屏蔽掉了原函数,转而直接返回我们指定的返回结果

3.2 其他用法

mocker.patch的函数定义

1
unittest.mock.patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, **kwargs)

常用参数含义:

  • target: 模拟对象的路径,参数必须是一个str,格式为’package.module.ClassName’,注意这里的格式一定要写对。如果对象和mock函数在同一个文件中,路径要加文件名
  • return_value: 模拟函数返回的结果
  • side_effect: 调用mock时的返回值,可以是函数,异常类,可迭代对象。当设置了该方法时,如果该方法返回值是DEFAULT,那么返回return_value的值,如果不是,则返回该方法的值。 return_value 和 side_effect 同时存在,side_effect会返回。(如果 side_effect 是异常类或实例时,调用模拟程序时将引发异常。如果 side_effect 是可迭代对象,则每次调用 mock 都将返回可迭代对象的下一个值。如果设置为函数时其具体表现会替换被mock函数)

4. MagicMock

在mock的过程中,有时我们需要构造相对复杂的返回值,比如对db操作函数的mock,返回值往往是一个对象。这时候常规做法我们就需要定义一个类,并且将其实例化。
这种做法较为麻烦,且不够灵活。Python提供了一个MagicMock方法,我们可以较为方便的构造我们想要的数据类型。

1
2
3
4
5
6
7
8
9
class TestTmpFunction(object):
def test_sum_with_magic_mock(self, mocker):
o1 = MagicMock(a=1)
print o1.a

o2 = MagicMock()
print list(o2)
o2.__iter__.return_value = [1, 2, 3]
print list(o2)

在工程实践中,我们一般对MagicMock在进行一次封装

1
2
3
4
5
6
7
8
9
10
11
def factory(attrs=None, **kwargs):
kwargs['return_value'] = True
o = MagicMock(**kwargs)

if not attrs:
return o

for k, v in attrs.items():
setattr(o, k, v)

return o

5. 数据驱动

某些时候,我们希望我们的单测可以覆盖多种逻辑分支,这时为每一种case都单独写一个测试明显也是不现实的。PyTest为单测提供了参数化功能,也就是数据驱动

1
2
3
4
5
6
7
8
9
class TestTmpFunction(object):
@pytest.mark.parametrize('a, b, sum_result', [
(1, 2, 3),
(2, 3, 5),
])
def test_sum_with_param(self, mocker, a, b, sum_result):
result = get_sum(a, b)
print result
assert result == sum_result

6. 代码覆盖率

PyTest提供了pytest-cov插件来实现代码覆盖率的统计.

6.1 基础用法

1
>>> pytest --cov --cov-report=xml

6.2 生成差异报表

1
>>> diff-cover coverage.xml --compare-branch=origin/master --html-report report.html --fail-under=80

Python单元测试
https://smartmalphite.github.io/2022/08/10/PythonNote/PythonUnitTest/
作者
Enbo Wang
发布于
2022年8月10日
许可协议