Python编写单元测试实践

Python编写单元测试,单元测试框架使用pytest,pytest比较轻量,使用简单。
mock功能通过unitest的mock模块实现。
下面对pytest和unittest.mock进行简单介绍。

pytest

官方文档为: https://docs.pytest.org/en/latest/contents.html

简单使用

使用pytest编写测试脚本相对简单,示例如下,该脚本文件名为: pytest_test.py

1
2
3
4
5
6
7
8
#!/usr/bin/env python
# -*- coding: utf-8 -*-
def add(a, b):
return a+b

def test_add():
res = add(1, 2)
assert res == 3

在终端运行 pytest pytest_test.py即可。结果如下:

====================== test session starts ===========
platform darwin -- Python 3.6.8, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /mock_test
collected 1 item    
pytest_test.py .                 [100%]

======================== 1 passed in 0.01 seconds=======================

pytest搜索测试脚本的规则为:

1. 如果命令行指定脚本目录或文件,则按照命令行的设置执行。
   如果命令行没有执行脚本路径,则按照testpaths(如果配置的话)和当前路径开始查找。
2. 递归查找测试脚本。
3. 查找符合test_*.py或*_test.py的文件。
4. 查找类外的以test为前缀方法或模块;
   查找以Test为前缀的类(且没有__init__方法)中以test为前缀的方法或模块。

按照上述规则命令测试脚本,使用assert关键字对结果进行校验即可。

关键字驱动

很多时候编写单测脚本时需要考虑使用关键字驱动,平时使用pytest.mark.parametrize实现,官方文档为https://docs.pytest.org/en/latest/parametrize.html 。使用简单,示例如下:

1
2
3
4
5
6
7
8
9
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest
@pytest.mark.parametrize(
"test_input,expected",
[("3+5", 8), ("2+4", 6), ("6*9", 42)],
)
def test_eval(test_input, expected):
assert eval(test_input) == expected

终端执行 pytest pytest_test.py,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
========== test session starts ===========
platform darwin -- Python 3.6.8, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /mock_test
collected 3 items
pytest_test.py ..F [100%]

================ FAILURES ================
___________ test_eval[6*9-42] ____________

test_input = '6*9', expected = 42

@pytest.mark.parametrize(
"test_input,expected",
[("3+5", 8), ("2+4", 6), ("6*9", 42)],
)
def test_eval(test_input, expected):
> assert eval(test_input) == expected
E AssertionError: assert 54 == 42
E + where 54 = eval('6*9')

pytest_test.py:16: AssertionError
======= 1 failed, 2 passed in 0.07 seconds ===================

pytest.mark.parametrize()共两个参数,第一个参数为数据驱动需要输入参数和预期结果,分别于测试函数入参对应;第二个参数为实际的驱动数据。如果驱动数据较多,可以定义列表变量,单独维护。

setup和teardown

pytest中实现初始化(setup)和销毁(teardown)功能也很简单,官网文档: https://docs.pytest.org/en/latest/xunit_setup.html 。平时使用过程中,主要使用测试类中方法级别的setup和teardown,其使用方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest
class TestRunAll():

def setup_method(self, method):
"""
setup any state tied to the execution of the given
method in a class. setup_method is invoked for every
test method of a class.
"""
pass

def teardown_method(self, method):
"""
teardown any state that was previously setup with a
setup_method call.
"""
pass

其他类型的定义可在官方文档查找。

MOCK

官方文档分别为: https://docs.python.org/3/library/unittest.mock.html

编写单元测试脚本时,主要使用patch进行mock,patch可以作为装饰器、上下文管理器(通过with关键字)使用。patch.object()可以对类的方法进行mock,这个是平时使用最多的方式。
如下例所示,我们需要测试use_a()方法,该方法通过type类型的不同值,调用A.judge()时使用不同的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from unittest.mock import patch


class A(object):
def judge(self, a, b):
return a+b
def use_a(type=None):
a = A()
if type=='A':
a.judge(1, 3)
else:
a.judge(2, 4)

@patch.object(A, 'judge', return_value=4)
def test_use(judge_mock):
use_a()
judge_mock.assert_called_with(2, 4)
use_a('A')
judge_mock.assert_called_with(1, 3)

注意:
patch作为装饰器使用时,它以自底向上的顺序将mock对象传递给被装置函数,
也就是说下例中module.ClassName1传递给MockClass1。
from unittest.mock import patch
@patch('module.ClassName2')
@patch('module.ClassName1')
def test(MockClass1, MockClass2):
     module.ClassName1()
     module.ClassName2()
     assert MockClass1 is module.ClassName1
     assert MockClass2 is module.ClassName2
     assert MockClass1.called
     assert MockClass2.called

patch.object()函数参数如下:

1
2
3
patch.object(target, attribute, new=DEFAULT, spec=None, 
create=False, spec_set=None, autospec=None, new_callable=None,
**kwargs)

target为类名称,attribute为需要mock的方法名。通过装饰器和with上下文管理器,使得mock范围为所装饰的函数或with范围内。

assert相关函数

mock类提供多个断定函数,如上例中assert_called_with(),下面对mock提供的mock函数进行汇总。详细信息可以参考官方对mock类的说明: https://docs.python.org/3/library/unittest.mock.html#the-mock-class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
assert_called(*args, **kwargs) 
# 断言该mock至少被调用过一次

assert_called_once(*args, **kwargs)
# 断言该mock仅被调用过一次

assert_called_with(*args, **kwargs)
# 断言该mock以指定的参数(*args, **kwargs)被调用

assert_called_once_with(*args, **kwargs)
#断言该mock以指定的参数(*args, **kwargs)被调用过一次

assert_any_call(*args, **kwargs)
# 断言该mock以指定的参数(*args, **kwargs)被调用过,不同于
# assert_called_with、assert_called_once_with判断的是最近一次的调用,
# assert_any_call判断的是mock有被调用过。

assert_has_calls(calls, any_order=False)
# 断言该mock按calls设置的方式被调用,如果any_order为False,则mock的调用
# 顺序必须与calls设置一致;如果any_order为True,则需要calls设置的方式必须
# 被调用过。

assert_not_called()
# 断言该mock没有被调用过