Pytest-fixture
3 Fixture
fixture 是在测试函数运行前后,由 pytest 执行的外壳函数。fixture 中的代码可以定制,满足多变的测试需求,包括定义传入测试中的数据集、配置测试前系统的初始状态、为批量测试提供数据源,等等。
@pytest.fixture() 装饰器用于声明函数是一个 fixture。如果测试函数的参数列表中包含 fixture 名,那么 pytest 会检测到,并在测试函数运行之前执行该 fixture。fixture 可以完成任务,也可以返回数据给测试函数。
1 @pytest.fixture() 2 def some_data(): 3 """Return answer to ultimate question.""" 4 return 42 5 6 7 def test_some_data(some_data): 8 """Use fixture return value in a test.""" 9 assert some_data == 42
测试用例 test_some_data() 的参数列表中包含一个 fixture 名 some_data,pytest 会以该名称搜索 fixture。pytest会优先搜索该测试所在的模块,然后搜索 conftest.py。
3.1 通过 conftest.py 共享 fixture
fixture 可以放在单独的测试文件里。如果你希望多个测试文件共享 fixture,可以在某个公共目录下新建一个 conftest.py 文件,将 fixture 放在其中。
如果你希望 fixture 的作用域仅限于某个测试文件,那么将它写在该测试文件里。你也可以在 tests 目录的子目录下创建新的 conftest.py 文件,低一级的 conftest.py 中的 fixture 可以供所在目录及其子目录下的测试使用。
尽管 conftest.py 是Python模块,但它不能被测试文件导入。import conftest 的用法是不允许出现的。conftest.py 被 pytest 视作一个本地插件库。可以把 tests/conftest.py 看成是一个供 tests 目录下所有测试使用的 fixture 仓库。
@pytest.fixture() def tasks_db(tmpdir): """Connect to db before tests, disconnect after.""" # Setup : start db tasks.start_tasks_db(str(tmpdir), 'tiny') yield # this is where the testing happens # Teardown : stop db tasks.stop_tasks_db()
fixture 函数会在测试之前运行,但如果 fixture 函数包含 yield,那么系统会在 yield 处停止,转而运行测试函数,等测试函数执行完毕后再回到 fixture,继续执行 yield 后面的代码。因此,可以将 yield 之前的代码视为配置(setup)过程,将 yield 之后的代码视为清理(teardown)过程。
3.2 使用 --setup-show 回溯 fixture 的执行过程
编写 fixture 时希望看到测试过程中执行的是什么,以及执行的先后顺序,pytest 提供的 --setup-show 选项可以实现这个功能。
fixture 名称前面的 F 和 S 代表的是 fixture 的作用范围,F 代表函数级别的作用范围,S 代表会话级别的作用范围。
3.3 使用 fixture 传递测试数据
fixture 非常适合存放测试数据,并且它可以返回任何数据。
1 @pytest.fixture() 2 def a_tuple(): 3 """Return something more interesting.""" 4 return (1, 'foo', None, {'bar': 23}) 5 6 7 def test_a_tuple(a_tuple): 8 """Demo the a_tuple fixture.""" 9 assert a_tuple[3]['bar'] == 32
除了堆栈跟踪的内容,pytest还给出了具体引起 assert 异常的函数参数值。fixture 作为测试函数的参数,也会被堆栈跟踪并纳入测试报告,假设 assert 异常(或任何类型的异常)就发生在 fixture 时,
首先堆栈会正确定位了 fixture 函数中发生的 assert 异常;其次,测试报告并没有被报告为 FAIL,而是报告为 ERROR。这个区分很清楚,如果测试结果为 FAIL,用户就知道失败时发生在核心测试函数内,而不是发生在测试依赖的 fixture。
3.4 指定 fixture 作用范围
fixture 包含一个叫 scope(作用范围)的可选参数,用于控制 fixture 执行配置和销毁逻辑的频率。@pytest.fixture() 的 scope 参数有四个特征值:function、class、module、session(默认值为function)。
- scope='function':函数级别的 fixture 每个测试函数只需要运行一次,配置代码在测试用例运行之前运行,销毁代码在测试用例运行之后运行。
- scope='class':类级别的 fixture 每个测试类只需要运行一次,无论测试类里有多少类方法都可以共享这个 fixture。
- scope='module':模块级别的 fixture 每个模块只需要运行一次,无论模块里有多少个测试函数、类方法或其他 fixture 都可以共享这个 fixture。
- scope='session':会话级别的 fixture 每次会话只需要运行一次,一次 pytest 会话中的所有测试函数、方法都可以共享这个 fixture。
import pytest @pytest.fixture(scope='function') def func_scope(): """A function scope fixture.""" @pytest.fixture(scope='module') def mod_scope(): """A module scope fixture.""" @pytest.fixture(scope='session') def sess_scope(): """A session scope fixture.""" @pytest.fixture(scope='class') def class_scope(): """A class scope fixture.""" def test_1(sess_scope, mod_scope, func_scope): """Test using session, module, and function scope fixtures.""" def test_2(sess_scope, mod_scope, func_scope): """Demo is more fun with multiple tests.""" @pytest.mark.usefixtures('class_scope') class TestSomething(): """Demo class scope fixtures.""" def test_3(self): """Test using a class scope fixture.""" def test_4(self): """Again, multiple tests are more fun."""
作用范围虽然是由 fixture 自身定义的,但还是要强调 scope 参数是在定义 fixture 时定义的,而不是在调用 fixture 时定义的,因此,使用 fixture 的测试函数是无法改变 fixture 的作用范围的。
fixture 只能使用同级别的 fixture,或比自己级别更高的 fixture。比如,函数级别的 fixture 可以使用同级别的 fixture,也可以使用类级别、模块级别、会话级别的 fixture,但不能反过来。
3.4.1 动态范围
在一些情况下,你可能想要在不改变代码的情况下修改 fixture 的范围。你可以通过给scope传递一个可调用对象来实现它(译者注:这里的可调用对象,其实就是一个内置的函数,通过传递一个函数来修改范围值,具体参见下面的例子)。
这个函数必须返回一个合法的范围名,这个函数会在夹具定义的时候被执行一次。这个函数必须有两个参数:fixture_name(字符串)和config(配置对象)。
这个特性在编写例如大量启动docker容器这种需要花费大量时间的夹具的时候非常有用,你可以使用命令行来控制在不同环境中启动容器的范围。
1 def determine_scope(fixture_name, config): 2 if config.getoption("--keep-containers", None): 3 return "session" 4 return "function" 5 6 @pytest.fixture(scope=determine_scope) 7 def docker_container(): 8 yield spawn_container()
比如下面生成 docker 容器函数 ,你可以使用命令行参数控制范围:
1 # conftest.py 内容 2 from time import sleep 3 4 import pytest 5 6 def determine_scope(fixture_name, config): 7 # 如果发现参数是 --keep-containers 8 if config.getoption("--scope_session", None): 9 return "session" 10 # 返回函数级别 11 return "function" 12 13 @pytest.fixture(scope=determine_scope) 14 def create_value(): 15 sleep(5) 16 17 def pytest_addoption(parser): 18 parser.addoption("--scope_session", action="store", default=None, 19 help="None") 20 21 # test_a1内容 22 23 def test_a1(create_value): 24 print('a1') 25 26 def test_a2(create_value): 27 print('a2') 28 29 def test_a3(create_value): 30 print('a3')
3.5 使用 usefixtures 指定 fixture
目前为止使用 fixture 的测试,都是在测试函数的参数列表重指定 fixture,实际上,也可以使用 @pytest.mark.usefixtures('fixture1', 'fixture2') 标记测试函数或类。
使用 usefixtures,需要在参数列表中指定一个或多个 fixture 字符串。这对测试函数来讲其实意义不大,但非常适合测试类。
1 @pytest.mark.usefixtures('class_scope') 2 class TestSomething(): 3 """Demo class scope fixtures."""
使用 usefixtures 和在测试方法中添加 fixture 参数,二者大体上是差不多的。区别之一在于只有后者才能够使用 fixture 的返回值。
3.6 为常用 fixture 添加 autouse 选项
可以通过指定 autouse=True 选项,使作用域内的测试函数都运行该 fixture。
1 @pytest.fixture(autouse=True, scope='session') 2 def footer_session_scope(): 3 """Report the time at the end of a session.""" 4 yield 5 now = time.time() 6 print('--') 7 print('finished : {}'.format(time.strftime('%d %b %X', time.localtime(now)))) 8 print('-----------------') 9 10 11 @pytest.fixture(autouse=True) 12 def footer_function_scope(): 13 """Report test durations after each function.""" 14 start = time.time() 15 yield 16 stop = time.time() 17 delta = stop - start 18 print('\ntest duration : {:0.3} seconds'.format(delta)) 19 20 21 def test_1(): 22 """Simulate long-ish running test.""" 23 time.sleep(1) 24 25 26 def test_2(): 27 """Simulate slightly longer test.""" 28 time.sleep(1.23)
3.7 为 fixture 重命名
fixture 的名字展示在使用它的测试或其他 fixture 函数的参数列表上,通常会和 fixture 函数名保持一致,但 pytest 也允许使用 @pytest.fixture() 的name参数对 fixture 重命名。
1 @pytest.fixture(name='lue') 2 def ultimate_answer_to_life_the_universe_and_everything(): 3 """Return ultimate answer.""" 4 return 42 5 6 7 def test_everything(lue): 8 """Use the shorter name.""" 9 assert lue == 42
可以为 pytest 指定 --fixtures 命令行选项,并提供所在测试文件名,pytest 将列举所有可供测试使用的 fixture,包括重命名的。
3.8 Fixture 的参数化
1 tasks_to_try = (Task('sleep', done=True), 2 Task('wake', 'brian'), 3 Task('breathe', 'BRIAN', True), 4 Task('exercise', 'BrIaN', False)) 5 6 task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 7 for t in tasks_to_try] 8 9 @pytest.fixture(params=tasks_to_try) 10 def a_task(request): 11 """Using no ids.""" 12 return request.param
fixture 参数列表中的 request 也是 pytest 内建的 fixture 之一,代表 fixture 的调用状态,它有一个 param 字段,会被 @pytest.fixture(params=tasks_to_try) 的 params 列表中的一个元素填充。
a_task 逻辑非常简单,仅以 request.param 作为返回值共测试使用。因为 task 对象列表包含四个 task 对象,所以 a_task 将被测试调用四次。
- 可以用相同的字符串列表为其指定id
1 @pytest.fixture(params=tasks_to_try, ids=task_ids) 2 def b_task(request): 3 """Using a list of ids.""" 4 return request.param
- ids参数也可以被指定为一个函数(前面是列表),共 pytest 生成 task 标识
def id_func(fixture_value): """A function for generating ids.""" t = fixture_value return 'Task({},{},{})'.format(t.summary, t.owner, t.done) @pytest.fixture(params=tasks_to_try, ids=id_func) def c_task(request): """Using a function (id_func) to generate ids.""" return request.param
ids=id_func,该函数将作用于 params 列表中的每一个元素。params 参数是一个 Task 对象列表,id_func() 将调用单个 Task 对象。
注:该描述中的列表代指可迭代对象(例如:列表、元组)等
这些 ids 可用于 -k
选择要运行的特定测试,当某个测试失败时,它们还将识别该特定测试。使用运行pytest --collect-only
将显示展示生成的 ids。
数字、字符串、布尔和 None
将在测试 ids 中使用它们通常的字符串表示形式。对于其他对象,pytest将根据参数名称生成字符串。
1 # content of test_ids.py 2 import pytest 3 4 5 @pytest.fixture(params=[0, 1], ids=["spam", "ham"]) 6 def a(request): 7 return request.param 8 9 10 def test_a(a): 11 pass 12 13 14 def idfn(fixture_value): 15 if fixture_value == 0: 16 return "eggs" 17 else: 18 return None 19 20 21 @pytest.fixture(params=[0, 1], ids=idfn) 22 def b(request): 23 return request.param 24 25 26 def test_b(b): 27 pass
$ pytest --collect-only =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 10 items <Module test_anothersmtp.py> <Function test_showhelo[smtp.gmail.com]> <Function test_showhelo[mail.python.org]> <Module test_ids.py> <Function test_a[spam]> <Function test_a[ham]> <Function test_b[eggs]> <Function test_b[1]> <Module test_module.py> <Function test_ehlo[smtp.gmail.com]> <Function test_noop[smtp.gmail.com]> <Function test_ehlo[mail.python.org]> <Function test_noop[mail.python.org]> ======================= 10 tests collected in 0.12s ========================
params=['a','b']