善用 pytest fixture factory 构建结构优秀的单元测试

2022-11-02 14:04:47 浏览数 (1)

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 使用用例:

代码语言:javascript复制
@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 来实现

代码语言:javascript复制
# 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 中不断添加即可,开发注意力成功从重复的工具构建转移到了真实用例。

0 人点赞