Why pytest ?
评价单元测试是否优秀,分支覆盖率是非常重要的指标,而覆盖率的决定因素除了开发自身的素质以外,足够低的用例构建成本也是必不可少。
对于 Python 应用,当项目逻辑复杂度较高时,单纯使用原生的 unittest
或者是 Django 提供的 TestCase
都会遇到一个恼人的问题:测试代码大部分工作在构建各种用例。幸好有 pytest
提供的 fixture
机制,可以较好的解决这个问题。
本文简单阐述个人的 pytest 使用实践,而关于 fixture
到底好在哪里或者具体使用方法,已经有比较多的文章做了更详细的介绍,我这里就不再赘述:
- https://blog.daftcode.pl/the-cleaning-hand-of-pytest-28f434f4b684
- https://salmonmode.github.io/2019/03/29/building-good-tests.html#dont-inherit-from-unittesttestcase-in-test-classes-either-directly-or-indirectly
理想的单元测试
从理论来讲,对于一个测试项,我们应该只需关心测试内容的输入和输出(或异常),并且最好能够放到一起管理维护,更形象地说,应该是下面这种伪代码的感觉
代码语言:javascript复制(输入, 预期输出)
(输入, 预期输出)
(输入, 预期输出)
(输入, 预期异常)
def test_some_content(输入, 预期输出):
if 正常:
assert some_process(输入) == 预期输出
else:
with should_raise(预期异常):
some_process(输入)
一个普通的 pytest
使用用例:
@pytest.fixture
def user():
return User(name="Chris", hair_color=Color("brown"))
@pytest.fixture(autouse=True)
def set_user(client, user):
client.set_user(user)
def test_set_user(client, user):
# client.get_user() returns another User object
assert client.get_user() == user
可以看出 pytest.fixture
固然能够足够强大,但是并不能完全解决问题:因为 fixture
在这里的用法是 静态 的,而对于我们理想的状态,用例中的输入输出,都应该是动态指定的。
一个简单的实践
所以,我们需要利用 fixture factory
来实现
# fixture 依旧保留了复用性
@pytest.fixture
def make_fake_resp():
def _make_fake_resp(input: str):
def _wrapper(*args, **kwargs):
return {"data": input}
return _wrapper
return _make_fake_resp
class TestUtils:
@pytest.mark.parametrize(
"input, expected",
[
# 正常情况
("abc", "xyz"),
# 异常情况,当然也可以多拆分一个测试项
("abc", ValueError),
],
)
def test_some_content(self, input, expected, make_fake_resp):
"""测试某些内容"""
with mock.patch('some-need-patching-refrences') as _patch:
# 利用 fixture factory 动态生成测试用例
_patch.side_effect = make_fake_resp(input)
if type(expected) is type and issubclass(expected, Exception):
with pytest.raises(expected):
# 主体测试逻辑
call_test(input)
else:
# 主体测试逻辑
assert call_test(input) == expected
这样一来,只要我在一开始构建好相关的工厂函数,添加更多的测试用例就只需在 parametrize
中不断添加即可,开发注意力成功从重复的工具构建转移到了真实用例。