Python Pytest
pytest
overview
Pytest 是用于测试 Python 应用的 Python 库。 它是鼻子测试和单元测试的替代方法。
使用以下命令安装 Pytest:
1
$ pip install pytest
运行 pytest
pytest 不带任何参数,将查看当前工作目录(或其他一些预配置的目录)以及测试文件的所有子目录,并运行找到的测试代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 运行当前目录中的所有测试文件。
$ pytest
# 通过指定名称作为参数来运行特定的测试文件。
$ pytest min_max_test.py
# 通过在`::`字符后提供其名称来运行特定功能。
$ pytest min_max_test.py::test_min
# 标记可用于对测试进行分组。 然后使用`pytest -m`运行一组标记的测试。
$ pytest -m smoke
# 使用表达式来运行与测试函数和类的名称匹配的测试。
$ pytest -k <expression>
basic
first test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# $ vim text_1.py
def func(x):
return x+1
def test_answer():
assert func(3) == 5
# ---------
# $ py.test test_1.py
# =================== FAILURES ===================
# _________________ test_answer __________________
# def test_answer():
# > assert func(3) == 5
# E assert 4 == 5
# E + where 4 = func(3)
# tests/test_1.py:5: AssertionError
# =========== short test summary info ============
# FAILED tests/test_1.py::test_answer - assert ...
# ============== 1 failed in 0.05s ===============
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# algo.py
def max(values):
_max = values[0]
for val in values:
if val > _max:
_max = val
return _max
def min(values):
_min = values[0]
for val in values:
if val < _min:
_min = val
return _min
# min_max_test.py
#!/usr/bin/env python3
import algo
def test_min():
values = (2, 3, 1, 4, 6)
val = algo.min(values)
assert val == 1
def test_max():
values = (2, 3, 1, 4, 6)
val = algo.max(values)
assert val == 6
# ---
# $ pytest min_max_test.py
# == test session starts ====
# platform win32 -- Python 3.7.0, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
# rootdir: C:\Users\Jano\Documents\pyprogs\pytest
# collected 2 items
# min_max_test.py [100%]
# == 2 passed in 0.03 seconds ==
pytest 跳过
使用跳过装饰器,我们可以跳过指定的测试。
- 跳过测试有多种原因。
- 例如,数据库/在线服务目前不可用,或者我们跳过了 Windows 上针对 Linux 的特定测试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# skipping.py
#!/usr/bin/env python3
import algo
import pytest
@pytest.mark.skip
def test_min():
values = (2, 3, 1, 4, 6)
val = algo.min(values)
assert val == 1
def test_max():
values = (2,
val = algo.max(values)
assert val == 6
# 在示例中,`test_min()`被跳过。
# $ pytest min_max_test.py
# ===== test session starts ====
# platform win32 -- Python 3.7.0, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
# rootdir: C:\Users\Jano\Documents\pyprogs\pytest
# collected 2 items
# min_max_test.py s. [100%]
# = 1 passed, 1 skipped in 0.04 seconds =
在测试文件名后面的输出中,s 代表跳过的和。 通过。
pytest 标记
使用标记将测试组织为单元。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# marking.py
#!/usr/bin/env python3
# pytest -m a marking.py
# pytest -m b marking.py
import pytest
@pytest.mark.a
def test_a1():
assert (1) == (1)
@pytest.mark.a
def test_a2():
assert (1, 2) == (1, 2)
@pytest.mark.a
def test_a3():
assert (1, 2, 3) == (1, 2, 3)
@pytest.mark.b
def test_b1():
assert "falcon" == "fal" + "con"
@pytest.mark.b
def test_b2():
assert "falcon" == f"fal{'con'}"
# 两组由标记 a 和 b 标识的测试。
# 这些单元由`pytest -m a marking.py`和`pytest -m b marking.py`运行。
Pytest 参数化测试
- 通过参数化测试,我们可以向断言中添加多个值。
- 使用
@pytest.mark.parametrize
标记。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# parametrized.py
#!/usr/bin/env python3
import algo
import pytest
@pytest.mark.parametrize("data, expected", [
((2, 3, 1, 4, 6), 1),
((5, -2, 0, 9, 12), -2),
((200, 100, 0, 300, 400), 0)])
def test_min(data, expected):
val = algo.min(data)
assert val == expected
@pytest.mark.parametrize("data, expected", [
((2, 3, 1, 4, 6), 6),
((5, -2, 0, 9, 12), 12),
((200, 100, 0, 300, 400), 400)])
def test_max(data, expected):
val = algo.max(data)
assert val == expected
# 使用多个输入数据测试这两个功能。
@pytest.mark.parametrize("data, expected", [
((2, 3, 1, 4, 6), 1),
((5, -2, 0, 9, 12), -2),
((200, 100, 0, 300, 400), 0)])
def test_min(data, expected):
val = algo.min(data)
assert val == expected
# 我们将两个值传递给测试函数:数据和期望值。 在我们的例子中,我们用三个数据元组测试`min()`函数。
# $ pytest parametrized.py
# ==== test session starts ====
# platform win32 -- Python 3.7.0, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
# rootdir: C:\Users\Jano\Documents\pyprogs\pytest
# collected 6 items
# parametrized.py ..... [100%]
# = 6 passed in 0.03 seconds ==
Pytest 输出告知有六次运行。
pytest 夹具
- 测试需要在一组已知对象的背景下进行。 这组对象称为测试夹具。
Fixtures
- the purpose of test fixtures is to provide a fixed baseline
- upon which tests can reliably and repeatedly execute.
- fixtures have explicit names
- and are activated by declaring their use from test function, modules, classes or whole projects. <- dependency injection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# algo.py
# 向`algo.py`模块添加了一个选择排序算法。
def sel_sort(data):
if not isinstance(data, list):
vals = list(data)
else:
vals = data
size = len(vals)
for i in range(0, size):
for j in range(i+1, size):
if vals[j] < vals[i]:
_min = vals[j]
vals[j] = vals[i]
vals[i] = _min
return vals
...
# `fixtures.py`
# 我们用夹具测试选择排序。
#!/usr/bin/env python3
import algo
import pytest
@pytest.fixture
def data():
return [3, 2, 1, 5, -3, 2, 0, -2, 11, 9]
# 我们的测试装置仅返回一些测试数据。
# 请注意,我们通过其名称引用此灯具:`data`。
def test_sel_sort(data):
sorted_vals = algo.sel_sort(data)
assert sorted_vals == sorted(data)
# 在`test_sel_sort()`函数中,我们将数据夹具作为函数参数传递。
# $ pytest fixtures.py
# ===== test session starts =====
# platform win32 -- Python 3.7.0, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
# rootdir: C:\Users\Jano\Documents\pyprogs\pytest
# collected 1 item
# fixtures.py [100%]
# == 1 passed in 0.02 seconds ===
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# vim test_4.py
import pytest
class Person:
def greet(self):
return "hello, there!"
@pytest.fixture
def person():
return Person()
def test_greet(person):
greeting = person.greet()
assert greeting == "hi, there!"
# pytest see test_greet needs a function argument named person
# find matching fixture-marked function named person.
# person() is called to create an instance
# test_greet(<Person instance) is called.
# ---------------------------
# $ py.test tests/test_4.py
# =================== FAILURES ===================
# def test_greet(person):
# greeting = person.greet()
# > assert greeting == "hi, there!"
# E AssertionError: assert 'hello, there!' == 'hi, there!'
# E - hi, there!
# E ? ^
# E + hello, there!
# E ? ^^^^
# tests/test_4.py:13: AssertionError
# =========== short test summary info ============
# FAILED tests/test_4.py::test_greet - Assertio...
# ============== 1 failed in 0.07s ===============
testing Exceptions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# $ vim test_sysexit.py
import pytest
def f():
raise SystemExit(1)
def test_mytest():
with pytest.raises(SystemExit):
f() # see if this will raise the Exception
# if raise the same Exception
# this test will pass.
# ---------------------------
# $ py.test tests/test_2.py
# tests/test_2.py . [100%]
# ============== 1 passed in 0.01s ===============
context-sensitive comparisons
will tell the different.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# $ vim text_3.py
def test_answer():
assert set(['0','1','2']) == set(['0','2','3'])
assert 'foo1' == 'foo2'
assert {'a':0,'b':1,'c':0} == {'a':0,'b':0,'d':0}
# ---------------------------
# $ py.test tests/test_3.py
# =================== FAILURES ===================
# _________________ test_answer __________________
# def test_answer():
# > assert set(['0','1','2']) == set(['0','2','3'])
# E AssertionError: assert {'0', '1', '2'} == {'0', '2', '3'}
# E Extra items in the left set:
# E '1'
# E Extra items in the right set:
# E '3'
# E Use -v to get the full diff
# E AssertionError: assert 'foo1' == 'foo2'
# E - foo2
# E ? ^
# E + foo1
# E ? ^
# tests/test_3.py:5: AssertionError
# =========== short test summary info ============
# FAILED tests/test_3.py::test_answer - Asserti...
# ============== 1 failed in 0.05s ===============
# (pgbackup) pgbackup[master !?] $
Pytest 布局
Python 测试可以多种方式组织。 测试可以集成在 Python 包中,也可以放在包外。
综合测试
在 Python 包中运行测试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 这种包装布局。 测试与软件包一起位于`tests`子目录中。
# setup.py
# utils
# │ algo.py
# │ srel.py
# │ __init__.py
# │
# └───tests
# algo_test.py
# srel_test.py
# __init__.py
# `setup.py`
#!/usr/bin/env python3
from setuptools import setup, find_packages
setup(name="utils", packages=find_packages())
# `utils/algo.py`
def sel_sort(data):
if not isinstance(data, list):
vals = list(data)
else:
vals = data
size = len(vals)
for i in range(0, size):
for j in range(i+1, size):
if vals[j] < vals[i]:
_min = vals[j]
vals[j] = vals[i]
vals[i] = _min
return vals
def max(values):
_max = values[0]
for val in values:
if val > _max:
_max = val
return _max
def min(values):
_min = values[0]
for val in values:
if val < _min:
_min = val
return _min
# `utils/srel.py`
def is_palindrome(val):
return val == val[::-1]
# 我们还有另一个模块,其中包含一个测试单词是否为回文的功能。
# `tests/algo_test.py`
#!/usr/bin/env python3
import utils.algo
import pytest
@pytest.fixture
def data():
return [3, 2, 1, 5, -3, 2, 0, -2, 11, 9]
def test_sel_sort(data):
sorted_vals = utils.algo.sel_sort(data)
assert sorted_vals == sorted(data)
def test_min():
values = (2, 3, 1, 4, 6)
val = utils.algo.min(values)
assert val == 1
def test_max():
values = (2, 3, 1, 4, 6)
val = utils.algo.max(values)
assert val == 6
# 这些是`utils.algo`模块的测试。 注意,我们使用完整的模块名称。
# `tests/srel_test.py`
#!/usr/bin/env python3
import utils.srel
import pytest
@pytest.mark.parametrize(
"word, expected",
[('kayak', True), ('civic', True), ('forest', False)])
def test_palindrome(word, expected):
val = utils.srel.is_palindrome(word)
assert val == expected
# 这是对`is_palindrome()`功能的测试。
# `utils/__init__.py`
# `utils/tests/__init__.py`
# 两个`__init__.py`文件均为空。
# $ pytest --pyargs utils
# === test session starts ===
# platform win32 -- Python 3.7.0, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
# rootdir: C:\Users\Jano\Documents\pyprogs\pytest\structure
# collected 6 items
# utils\tests\algo_test.py .. [ 50%]
# utils\tests\srel_test.py .. [100%]
# 6 passed in 0.06 seconds =
# 我们使用`pytest --pyargs utils`命令运行测试。
外部测试
下一个示例显示了应用源布局,其中测试未集成在包内。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# setup.py
# src
# └───utils
# │ algo.py
# │ srel.py
# tests
# algo_test.py
# srel_test.py
# 在这种布局中,我们在源代码树之外进行测试。 请注意,不需要`__init__.py`文件。
$ set PYTHONPATH=src
$ pytest
# 我们设置`PYTHONPATH`并运行 pytest。
pytest-mock
- This plugin provides a mocker fixture
- a thin-wrapper around the patching API provided by the mock package.
unittest.mock
Python3.3 新增用来在单元测试的时候进行 mock 操作的 unittest.mock 模块。
常见用法
定义返回值 mock.return_value
1
2
3
4
5
6
7
8
from unittest.mock import MagicMock
mock = MagicMock(return_value=3)
# or
mock = MagicMock()
mock.return_value = 3
>>> mock()
3
定义变化的返回值 side_effect=xxx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
mock = MagicMock(side_effect=[1, 2, 3])
# or
mock = MagicMock()
mock.side_effect = [1, 2, 3]
>>> mock()
1
>>> mock()
2
>>> mock()
3
>>> mock()
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "/xxx/lib/python3.6/unittest/mock.py", line 939, in __call__
# return _mock_self._mock_call(*args, **kwargs)
# File "/xxx/lib/python3.6/unittest/mock.py", line 998, in _mock_call
# result = next(effect)
# StopIteration
def side_effect(arg=1):
return arg
m = MagicMock(side_effect=side_effect)
>>> m()
1
>>> m(1)
1
>>> m(2)
2
定义抛出异常 side_effect = KeyError
1
2
3
4
5
6
7
8
9
10
m.side_effect = KeyError
m()
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "/xxx/python3.6/unittest/mock.py", line 939, in __call__
# return _mock_self._mock_call(*args, **kwargs)
# File "/xxx/python3.6/unittest/mock.py", line 995, in _mock_call
# raise effect
# KeyError
mock 变量/属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# example.py:
class FooBar:
def __init__(self):
self.msg = 'test'
def hello(self):
return 'hello'
foo = {}
def bar():
return foo
# return {}
def foobar():
return FooBar().hello()
# return 'hello'
fb = FooBar()
def hello():
return fb.msg
# self.msg = 'test'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from unittest.mock import patch
m = MagicMock()
m.test = MagicMock(return_value=233)
>>> m()
# <MagicMock name='mock()' id='4372854824'>
>>> m.test
# <MagicMock name='mock.test' id='4372854768'>
>>> m.test()
233
import example
>>> example.foo
{}
>>> example.hello()
'test'
>>> with patch.object(example, 'foo', {'lalala': 233}):
example.foo
{'lalala': 233}
>>> example.foo
{}
>>> with patch.object(example.fb, 'msg', 666):
example.hello()
666
>>> example.hello()
'test'
mock dict
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> foo = {'a': 233}
>>> foo['a']
233
>>> with patch.dict(foo, {'a': 666, 'b': 222}):
print(foo['a']) # 666
print(foo['b']) # 222
>>> foo['a']
233
>>> 'b' in foo
False
常用检查方法
- mock 的对象拥有一些可以用于单元测试的检查方法,
- 可以用来测试 mock 对象的调用情况。
检查调用次数
1
2
3
4
5
6
7
8
9
10
# 待检查的 mock 对象:
m = MagicMock()
>>> m(1)
<MagicMock name='mock()' id='4372904760'>
>>> m(2)
<MagicMock name='mock()' id='4372904760'>
>>> m(3)
<MagicMock name='mock()' id='4372904760'>
>>>
.called
: 是否被调用过
1
2
>>> m.called
True
.call_count
: 获取调用次数
1
2
>>> m.call_count
3
.assert_called():
- 检查是否被调用过
- 如果没有被调用过,则会抛出 AssertionError 异常
1
2
>>> m.assert_called()
>>>
.assert_called_once()
: 确保调用过一次
- 如果没调用或多于一次,否则抛出 AssertionError 异常
1
2
3
4
5
6
>>> m.assert_called_once()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/xxx/lib/python3.6/unittest/mock.py", line 795, in assert_called_once
raise AssertionError(msg)
AssertionError: Expected 'mock' to have been called once. Called 3 times.
.assert_not_called()
:
- 确保没被调用过,否则抛出 AssertionError 异常
1
2
3
4
5
6
>>> m.assert_not_called()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/xxx/lib/python3.6/unittest/mock.py", line 777, in assert_not_called
raise AssertionError(msg)
AssertionError: Expected 'mock' to not have been called. Called 3 times.
检查调用时使用的参数¶
待检查的 mock 对象:
1
2
3
4
m = MagicMock()
>>> m(1, 2, foo='bar')
<MagicMock name='mock()' id='4372980792'>
.call_args: 最后调用时的参数
- 最后一次调用时使用的参数,未调用则返回 None
1
2
>>> m.call_args
call(1, 2, foo='bar')
.assert_called_once_with(*args, **kwargs):
确保只调用过一次,并且使用特定参数调用
1
2
3
4
5
6
7
8
9
10
11
>>> m.assert_called_once_with(1, 2, foo='bar')
>>> m(2)
<MagicMock name='mock()' id='4372980792'>
>>> m.assert_called_once_with(1, 2, foo='bar')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/xxx/lib/python3.6/unittest/mock.py", line 824, in assert_called_once_with
raise AssertionError(msg)
AssertionError: Expected 'mock' to be called once. Called 2 times.
.assert_any_call(*args, **kwargs)
: 检查某次用特定参数进行过调用
1
2
>>> m.assert_any_call(1, 2, foo='bar')
>>>
.assert_called_with(*args, **kwargs)
: 检查最后一次调用时使用的参数
1
2
3
4
5
6
7
8
9
10
11
>>> m.assert_called_with(2)
>>>
>>> m.assert_called_with(1, 2, foo='bar')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/xxx/lib/python3.6/unittest/mock.py", line 814, in assert_called_with
raise AssertionError(_error_message()) from cause
AssertionError: Expected call: mock(1, 2, foo='bar')
Actual call: mock(2)
>>>
.call_args_list: 所有调用时使用的参数列表
1
2
3
4
5
6
7
8
>>> m.call_args_list
[call(1, 2, foo='bar'), call(2)]
>>> m(3)
<MagicMock name='mock()' id='4372980792'>
>>> m.call_args_list
[call(1, 2, foo='bar'), call(2), call(3)]
.assert_has_calls(calls, any_order=False):
- 检查某几次调用时使用的参数
- any_order 为 False 时必须是挨着的调用顺序, 可以是中间的几次调用
- any_order 为 True 时 calls 中的记录可以是无序的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> from unittest.mock import call
>>> m.call_args_list
[call(1, 2, foo='bar'), call(2), call(3)]
>>> m.assert_has_calls([call(2), call(3)])
>>>
>>> m.assert_has_calls([call(3), call(2)])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/xxx/lib/python3.6/unittest/mock.py", line 846, in assert_has_calls
) from cause
AssertionError: Calls not found.
Expected: [call(3), call(2)]
Actual: [call(1, 2, foo='bar'), call(2), call(3)]
>>> m.assert_has_calls([call(3), call(2)], any_order=True)
>>>
.method_calls: mock 对象的方法调用记录
1
2
3
4
5
>>> m.test_method(2, 3, 3)
<MagicMock name='mock.test_method()' id='4372935456'>
>>> m.method_calls
[call.test_method(2, 3, 3)]
.mock_calls:
- 记录 mock 对象的所有调用
- 包含方法、magic method 以及返回值 mock
1
2
3
4
5
>>> m.mock_calls
[call(1, 2, foo='bar'), call(2), call(3), call.test_method(2, 3, 3)]
>>> m.call_args_list
[call(1, 2, foo='bar'), call(2), call(3)]
手动重置 mock 调用记录
可以使用 .reset_mock() 重置 mock 对象记录的调用记录:
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> m.mock_calls
[call(1, 2, foo='bar'), call(2), call(3), call.test_method(2, 3, 3)]
>>> m.call_args_list
[call(1, 2, foo='bar'), call(2), call(3)]
>>> m.reset_mock()
>>> m.call_args_list
[]
>>> m.mock_calls
[]
>>>
.
This post is licensed under CC BY 4.0 by the author.
Comments powered by Disqus.