本文选自测试人社区
pytest架构是什么?
首先,来看一个 pytest 的例子:
def test_a():
print(123)
collected 1 item
test_a.py . [100%]
============ 1 passed in 0.02s =======================
输出结果很简单:收集到 1 个用例,并且这条测试用例执行通过。
此时思考两个问题:
- pytest 如何收集到用例的?
- pytest 如何把 python 代码,转换成 pytest 测试用例(又称 item) ?
pytest如何做到收集到用例的?
这个很简单,遍历执行目录,如果发现目录的模块中存在符合“ pytest 测试用例要求的 python 对象”,就将之转换为 pytest 测试用例。
比如编写以下 hook 函数:
def pytest_collect_file(path, parent):
print("hello", path)
hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\__init__.py
hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\conftest.py
hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\test_a.py
会看到所有文件内容。
如何构造pytest的item?
pytest 像是包装盒,将 python 对象包裹起来,比如下图:
当写好 python 代码时:
def test_a:
print(123)
会被包裹成 Function :
<Function test_a>
可以从 hook 函数中查看细节:
def pytest_collection_modifyitems(session, config, items):
pass
于是,理解包裹过程就是解开迷题的关键。pytest 是如何包裹 python 对象的?
下面代码只有两行,看似简单,但暗藏玄机!
def test_a:
print(123)
把代码位置截个图,如下:
我们可以说,上述代码是处于“testcase包”下的 “test_a.py模块”的“test_a函数”, pytest 生成的测试用例也要有这些信息:
处于“testcase包”下的 “test_a.py模块”的“test_a测试用例:
把上述表达转换成下图:
pytest 使用 parent 属性表示上图层级关系,比如 Module 是 Function 的上级, Function 的 parent 属性如下:
<Function test_a>:
parent: <Module test_parse.py>
当然 Module 的 parent 就是 Package:
<Module test_parse.py>:
parent: <Package tests>
注意大小写:Module 是 pytest 的类,用于包裹 python 的 module 。Module 和 module 表示不同意义。
这里科普一下,python 的 package 和 module 都是真实存在的对象,你可以从 obj 属性中看到,比如 Module 的 obj
属性如下:
如果理解了 pytest 的包裹用途,非常好!我们进行下一步讨论:如何构造 pytest 的 item ?
以下面代码为例:
def test_a:
print(123)
构造 pytest 的 item ,需要:
- 构建 Package
- 构建 Module
- 构建 Function
以构建 Function 为例,需要调用其from_parent()
方法进行构建,其过程如下图:
从函数名from_parent
,就可以猜测出,“构建 Function”一定与其 parent 有不小联系!又因为 Function 的 parent
是 Module :根据下面 Function 的部分代码(位于 python.py 文件):
class Function(PyobjMixin, nodes.Item):
# 用于创建测试用例
@classmethod
def from_parent(cls, parent, **kw):
"""The public constructor."""
return super().from_parent(parent=parent, **kw)
# 获取实例
def _getobj(self):
assert self.parent is not None