Pytest 固件:显式、模块化、可扩展
purpose of test fixtures 是提供一个固定的基线,在此基础上测试可以可靠地重复执行。Pytest 固件比传统的XUnit 的setup/teardown功能提供了显著的改进:
固件有明确的名称,通过声明它们在测试函数、模块、类或整个项目中的使用来激活。
固件以模块化的方式实现,因为每个固件名称触发一个 固件功能 , 可以使用其他固件。
固件管理规模从简单的单元扩展到复杂的功能测试,允许根据配置和组件选项参数化固件和测试,或者跨功能、类、模块或整个测试会话范围重复使用固件。
此外,pytest继续支持经典的Xunit-style setup. 你可以混合这两种样式,根据喜好,逐步从经典样式转移到新样式。你也可以从现有的 unittest.TestCase style 或 nose based 项目开始。
Fixtures 作为函数参数 Fixtures as function arguments
测试函数可以通过将fixture对象命名为输入参数来接收它们。对于每个参数名,具有该名称的fixture函数提供fixture对象。通过用@pytest.fixture标记fixture函数来注册fixture函数 . 让我们来看一个简单的独立测试模块,它包含一个fixture和一个使用fixture的测试函数:
1 2 3 4 5 6 7 8 9 10 11 12 import pytest@pytest.fixture def smtp_connection (): import smtplib return smtplib.SMTP("smtp.gmail.com" , 587 , timeout=5 ) def test_ehlo (smtp_connection ): response, msg = smtp_connection.ehlo() assert response == 250 assert 0
这里, test_ehlo 需要 smtp_connection 固件值。Pytest将发现并调用 @pytest.fixture 标记 smtp_connection 固件函数。运行测试的方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ pytest test_smtpsimple.py =========================== test session starts ============================ platform linux -- Python 3. x.y, pytest-4. x.y, py-1. x.y, pluggy-0. x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item test_smtpsimple.py F [100 %] ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ smtp_connection = <smtplib.SMTP object at 0xdeadbeef > def test_ehlo (smtp_connection ): response, msg = smtp_connection.ehlo() assert response == 250 > assert 0 E assert 0 test_smtpsimple.py:11 : AssertionError ========================= 1 failed in 0.12 seconds =========================
在失败的回溯中,我们看到测试函数是用 smtp_connection 参数、 fixture函数创建的实例smtplib.SMTP()来调用的。测试功能失败是故意使用了 assert 0 . pytest 以这种方式来调用测试函数:
pytest找到这个 test_ehlo 因为 test_ 前缀。这个测试函数需要一个名为 smtp_connection . 通过查找名为 smtp_connection 标记的固件函数,找到一个匹配的固件函数.
smtp_connection() 通过创建实例来调用。
test_ehlo(<smtp_connection instance>) 在测试函数的最后一行调用并失败。
请注意,如果你拼错了一个函数参数,或者希望使用一个不可用的参数,你将看到一个错误,其中包含一个可用函数参数列表。
注解
你可以随时发布:
1 pytest --fixtures test_simplefactory.py
查看可用的固件(带引线的固件 _ 仅当添加 -v 选择权。
固件:依赖注入的主要示例 fixtures:a prime example of dependency injection
fixture 是 pytest 中的一个强大功能,它通过提供预初始化的对象来实现依赖注入。依赖注入 是一种设计模式,用于减少代码之间的耦合,使代码更加模块化和可测试。
fixture 可以定义一些预初始化的对象 或环境设置 ,这些对象或设置可以在多个测试函数中复用。测试函数可以通过参数自动接收这些 fixture,而不需要关心这些对象是如何导入、初始化和清理的 。
在依赖注入中,依赖关系(即所需的对象或资源)是由外部提供的,而不是由对象自己创建的。fixture 函数在这里扮演了 注入器 (injector)的角色,负责创建和提供这些依赖对象。
测试函数是 消费者 (consumers),它们通过参数接收这些依赖对象,并使用它们进行测试。
小结:
fixture 是 pytest 中实现依赖注入的一种方式。fixture 函数负责创建和管理依赖对象,测试函数通过参数接收这些对象。
使用 fixture 可以减少测试代码中的重复,提高代码的可维护性和可测试性。
通过 yield 关键字,fixture 可以在测试前后执行设置和清理操作,确保每个测试都在干净的环境中运行。
conftest.py 共享固件功能 conftest.py: sharing fixture functions
如果在实现测试的过程中,你意识到要使用来自多个测试文件的fixture函数,可以将其移动到 conftest.py 文件。你不需要导入要在测试中使用的固件,Pytest会发现并自动获取它。fixture函数的发现从测试类开始,然后是测试模块,然后 conftest.py 文件,最后是内置插件和第三方插件。
你也可以使用 conftest.py 文件去实现 本地目录级插件 local per-directory plugins。
Local :这些插件仅影响定义它们的目录及其子目录。
Per-Directory :每个目录可以有自己的 conftest.py 文件,从而实现不同目录间的独立配置。
Plugins :这些插件可以包含 fixture、hook 实现、自定义命令行选项等,用于扩展 pytest 的功能。
共享测试数据 Sharing test data
如果你想让来自文件的测试数据对你的测试可用,一个很好的方法是将这些数据加载到一个固件中,供测试使用。这利用了pytest的自动缓存机制。
另一个好方法是将数据文件添加到 tests 文件夹中. 这里也有可用的插件社区可以用来帮助管理这方面的testing e.g. pytest-datadir 和 pytest-datafiles (感觉不怎么会用到,不仅需要安装包,这种路径、文件名的配置,不适合做大型项目吧)。
scope:在类、模块或会话中跨测试共享一个fixture实例 Scope: sharing a fixture instance across tests in a class, module or session
依赖于连接性的需要访问网络的fixtures,通常创建成本很高 。扩展前面的示例,我们可以给 @pytest.fixture 调用添加一个 scope="module" 参数,引起被装饰的 smtp_connection 固件函数在每个测试模块中只调用一次(默认情况下,每个测试调用一次 function)因此,一个测试模块中的多个测试功能将接收相同的 smtp_connection 固件实例(不必每次都实例化,也就是不用每次都去创建连接访问网络),节省时间提高效率。 对于scope可能值是: function , class , module , package 或 session .
下一个示例将fixture函数放入单独的 conftest.py 文件,以便目录中多个测试模块的测试可以访问fixture函数:
1 2 3 4 5 6 7 import pytestimport smtplib@pytest.fixture(scope="module" ) def smtp_connection (): return smtplib.SMTP("smtp.gmail.com" , 587 , timeout=5 )
固件的名称同样是 smtp_connection ,你可以通过列出名字smtp_connection作为一个在任何测试或fixture函数的输入参数(在 conftest.py 所在的目录中,或者所在的目录下)来访问它的结果 :
1 2 3 4 5 6 7 8 9 10 11 def test_ehlo (smtp_connection ): response, msg = smtp_connection.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg assert 0 def test_noop (smtp_connection ): response, msg = smtp_connection.noop() assert response == 250 assert 0
我们故意插入失败 assert 0 语句以检查正在进行的操作,现在可以运行测试:
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 $ pytest test_module.py =========================== test session starts ============================ platform linux -- Python 3. x.y, pytest-4. x.y, py-1. x.y, pluggy-0. x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py FF [100 %] ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ smtp_connection = <smtplib.SMTP object at 0xdeadbeef > def test_ehlo (smtp_connection ): response, msg = smtp_connection.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg > assert 0 E assert 0 test_module.py:6 : AssertionError ________________________________ test_noop _________________________________ smtp_connection = <smtplib.SMTP object at 0xdeadbeef > def test_noop (smtp_connection ): response, msg = smtp_connection.noop() assert response == 250 > assert 0 E assert 0 test_module.py:11 : AssertionError ========================= 2 failed in 0.12 seconds =========================
你看这两个 assert 0 失败,更重要的是,你也可以看到相同的(module-scoped模块范围) smtp_connection对象被传递到两个测试函数中,因为pytest在回溯中显示了传入的参数值。因此,两个测试函数使用 smtp_connection 像单个实例一样快速运行,因为它们重用同一个实例。
如果你决定希望有一个会话范围装饰的 smtp_connection实例, 例如,你可以简单地声明它:
1 2 3 4 5 @pytest.fixture(scope="session" ) def smtp_connection (): ...
最后, class 作用域将在每个测试class中调用fixture一次 .
注解
pytest一次只缓存一个fixture实例。这意味着当使用参数化固件时,pytest可以在给定的范围内多次调用固件。
首先实例化更大范围的固件 Higher-scoped fixtures are instantiated first
在特性的功能请求中,更高范围的固件(例如 session )先实例化,然后再实例化范围较低的固件(例如 function 或 class )。相同范围内固件的相对顺序遵循测试函数中声明的顺序,并尊重固件之间的依赖关系。
考虑下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @pytest.fixture(scope="session" ) def s1 (): pass @pytest.fixture(scope="module" ) def m1 (): pass @pytest.fixture def f1 (tmpdir ): pass @pytest.fixture def f2 (): pass def test_foo (f1, m1, f2, s1 ): ...
由 test_foo 所请求的固件将按以下顺序实例化:
s1 :是范围最高的固件 (session )
m1 :是第二高范围固件 (module )
tmpdir 是一个 function 范围固件, f1需要 :因为它是f1的一个依赖项,此时它需要实例化 .
f1 是第一个在 test_foo 参数列表内的 function -范围固件。
f2 是最后一个在 test_foo 参数列表内的 function -范围固件。
固件定型/执行拆卸代码 Fixture finalization / executing teardown code
当fixture超出范围时,pytest支持fixture特定的定稿代码的执行。通过使用 yield 语句而不是 return , yield 语句之后的所有代码当做是teardown代码:
1 2 3 4 5 6 7 8 9 10 11 import smtplibimport pytest@pytest.fixture(scope="module" ) def smtp_connection (): smtp_connection = smtplib.SMTP("smtp.gmail.com" , 587 , timeout=5 ) yield smtp_connection print ("teardown smtp" ) smtp_connection.close()
这个 print 和 smtp.close() 语句将在模块中的最后一个测试完成执行后执行,而不管测试的异常状态如何。
让我们执行它:
1 2 3 4 $ pytest -s -q --tb=no FFteardown smtp 2 failed in 0.12 seconds
我们看到了 smtp_connection 实例在两个测试完成执行后完成。请注意,如果我们用 scope='function'来声明固件函数, 然后在每个测试周围进行fixture setup和clearup。无论哪种情况,测试模块本身都不需要更改或了解这些固件setup的细节。
请注意,我们还可以无缝地使用 yield 语法与 with 声明:
1 2 3 4 5 6 7 8 9 import smtplibimport pytest@pytest.fixture(scope="module" ) def smtp_connection (): with smtplib.SMTP("smtp.gmail.com" , 587 , timeout=5 ) as smtp_connection: yield smtp_connection
这个 smtp_connection 连接将在测试完成执行后关闭,因为当 with 语句结束时 smtp_connection 对象会自动关闭。
请注意,如果一个异常在 setup 代码(在 yield 关键字前)中发生,那么 teardown 代码(在 yield 后)不会被调用。
执行 teardown 代码的替代选项是利用 addfinalizer 方法(用request-context对象注册终结函数)。
这里是 smtp_connection 固件变化地使用addfinalizer 清理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import smtplibimport pytest@pytest.fixture(scope="module" ) def smtp_connection (request ): smtp_connection = smtplib.SMTP("smtp.gmail.com" , 587 , timeout=5 ) def fin (): print ("teardown smtp_connection" ) smtp_connection.close() request.addfinalizer(fin) return smtp_connection
相似地, yield 和 addfinalizer 方法都是通过在测试结束后调用它们的代码来工作,但是 addfinalizer 有两个关键差异点区别于 yield :
可以注册多个终结器函数。
无论 fixture 的 setup 代码是否引发异常,finalizers 终结器都会被调用(面试题常问)。这对于正确关闭由固件创建的所有资源非常方便,即使其中一个资源未能创建/获取:
1 2 3 4 5 6 7 8 @pytest.fixture def equipments (request ): r = [] for port in ('C1' , 'C3' , 'C28' ): equip = connect(port) request.addfinalizer(equip.disconnect) r.append(equip) return r
在上面的示例中,如果 "C28" 异常失败, "C1" 和 "C3" 仍将正确关闭。
当然,如果在 addfinalizer 函数之前发生异常,则不会执行它 。
Fixtures 可以检查请求的测试上下文 Fixtures can introspect the requesting test context
固件函数可以接受 request 对象内省“requesting”测试函数、类或模块上下文。进一步扩展前一个 smtp_connection fixture示例,让我们从使用fixture的测试模块中读取一个可选的服务器URL:
1 2 3 4 5 6 7 8 9 10 11 import pytestimport smtplib@pytest.fixture(scope="module" ) def smtp_connection (request ): server = getattr (request.module, "smtpserver" , "smtp.gmail.com" ) smtp_connection = smtplib.SMTP(server, 587 , timeout=5 ) yield smtp_connection print ("finalizing %s (%s)" % (smtp_connection, server)) smtp_connection.close()
我们使用 request.module 属性来选择获取一个来自测试模块的smtpserver 属性。如果我们再执行一次,没有什么改变:
1 2 3 4 $ pytest -s -q --tb=no FFfinalizing <smtplib.SMTP object at 0xdeadbeef > (smtp.gmail.com) 2 failed in 0.12 seconds
让我们快速创建另一个测试模块,该模块在其模块命名空间中实际设置服务器URL:
1 2 3 4 5 6 smtpserver = "mail.python.org" def test_showhelo (smtp_connection ): assert 0 , smtp_connection.helo()
运行它:
1 2 3 4 5 6 7 8 9 10 $ pytest -qq --tb=short test_anothersmtp.py F [100 %] ================================= FAILURES ================================= ______________________________ test_showhelo _______________________________ test_anothersmtp.py:5 : in test_showhelo assert 0 , smtp_connection.helo() E AssertionError: (250 , b'mail.python.org' ) E assert 0 ------------------------- Captured stdout teardown ------------------------- finalizing <smtplib.SMTP object at 0xdeadbeef > (mail.python.org)
这个 smtp_connection fixture函数从模块名称空间中获取邮件服务器名称。
工厂函数作为固件 Factories as fixtures
“工厂作为固件”模式有助于在单个测试中多次需要固件结果的情况下,固件不直接返回数据,而是返回一个生成数据的函数,然后可以在测试中多次调用此函数。(工厂函数 :返回一个生成数据对象的函数,而不是直接返回数据对象。)
工厂可以根据需要设置参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @pytest.fixture def make_customer_record (): def _make_customer_record (name ): return { "name" : name, "orders" : [] } return _make_customer_record def test_customer_records (make_customer_record ): customer_1 = make_customer_record("Lisa" ) customer_2 = make_customer_record("Mike" ) customer_3 = make_customer_record("Meredith" )
如果工厂创建的数据需要管理,则固件可以处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @pytest.fixture def make_customer_record (): created_records = [] def _make_customer_record (name ): record = models.Customer(name=name, orders=[]) created_records.append(record) return record yield _make_customer_record for record in created_records: record.destroy() def test_customer_records (make_customer_record ): customer_1 = make_customer_record("Lisa" ) customer_2 = make_customer_record("Mike" ) customer_3 = make_customer_record("Meredith" )
为什么要使用工厂作为固件?
灵活性 :可以在测试中多次调用工厂函数,每次生成新的数据对象。
参数化 :工厂函数可以根据传入的参数生成不同的数据对象。
资源管理 :如果生成的数据对象需要管理(例如,需要在测试结束后销毁),固件可以处理这些管理任务。
参数化固件 Parametrizing fixtures
fixture函数可以参数化,在这种情况下,它们将被多次调用,每次执行一组相关的测试,即测试依赖于该fixture。测试函数通常不需要知道它们的重新运行。固件参数化有助于为组件编写详尽的功能测试,这些组件本身可以通过多种方式进行配置。
扩展前面的示例,我们可以标记fixture以创建两个 smtp_connection fixture实例,它将导致使用fixture的所有测试运行两次。fixture函数通过特殊的 request 对象来获取每个参数:
1 2 3 4 5 6 7 8 9 10 11 import pytestimport smtplib@pytest.fixture(scope="module" , params=["smtp.gmail.com" , "mail.python.org" ] )def smtp_connection (request ): smtp_connection = smtplib.SMTP(request.param, 587 , timeout=5 ) yield smtp_connection print ("finalizing %s" % smtp_connection) smtp_connection.close()
主要变化是 params 具有 @pytest.fixture ,fixture函数将执行的每个值的列表,可以通过 request.param访问每一个值 . 无需更改测试函数代码。让我们再运行一次:
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 $ pytest -q test_module.py FFFF [100 %] ================================= FAILURES ================================= ________________________ test_ehlo[smtp.gmail.com] _________________________ smtp_connection = <smtplib.SMTP object at 0xdeadbeef > def test_ehlo (smtp_connection ): response, msg = smtp_connection.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg > assert 0 E assert 0 test_module.py:6 : AssertionError ________________________ test_noop[smtp.gmail.com] _________________________ smtp_connection = <smtplib.SMTP object at 0xdeadbeef > def test_noop (smtp_connection ): response, msg = smtp_connection.noop() assert response == 250 > assert 0 E assert 0 test_module.py:11 : AssertionError ________________________ test_ehlo[mail.python.org] ________________________ smtp_connection = <smtplib.SMTP object at 0xdeadbeef > def test_ehlo (smtp_connection ): response, msg = smtp_connection.ehlo() assert response == 250 > assert b"smtp.gmail.com" in msg E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING' test_module.py:5 : AssertionError -------------------------- Captured stdout setup --------------------------- finalizing <smtplib.SMTP object at 0xdeadbeef > ________________________ test_noop[mail.python.org] ________________________ smtp_connection = <smtplib.SMTP object at 0xdeadbeef > def test_noop (smtp_connection ): response, msg = smtp_connection.noop() assert response == 250 > assert 0 E assert 0 test_module.py:11 : AssertionError ------------------------- Captured stdout teardown ------------------------- finalizing <smtplib.SMTP object at 0xdeadbeef > 4 failed in 0.12 seconds
我们看到我们的两个测试函数分别运行两次,针对的是不同的 smtp_connection 实例。还要注意的是, 在test_ehlo中mail.python.org连接第二次测试失败,因为预期的服务器字符串与实际获取的字符串不同。
pytest将构建一个字符串,该字符串是参数化fixture中每个fixture值的测试ID,例如在在上面的例子中的test_ehlo[smtp.gmail.com] 和 test_ehlo[mail.python.org] 。这些ID可用于 -k 选择要运行的特定案例,当某个案例失败时,它们还将识别该特定案例。使用pytest --collect-only 运行将显示生成的ID。
关键字 ids 数字、字符串、布尔值和None将在测试ID中使用它们通常的字符串表示形式。对于其他对象,pytest将根据参数名生成字符串。在一个测试ID中,可以通过使用 ids 关键字 参数来为一个确定的fixture值定制字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import pytest@pytest.fixture(params=[0 , 1 ], ids=["spam" , "ham" ] ) def a (request ): return request.param def test_a (a ): pass def idfn (fixture_value ): if fixture_value == 0 : return "eggs" else : return None @pytest.fixture(params=[0 , 1 ], ids=idfn ) def b (request ): return request.param def test_b (b ): pass
上面显示了如何 ids 可以是要使用的字符串列表,也可以是将使用fixture值调用的函数,然后必须返回要使用的字符串。在后一种情况下,如果函数返回 None 然后将使用pytest的自动生成的ID。
运行上述测试将导致使用以下测试ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 python -m pytest .\test_ids.py --collect-only ============================================================================== test session starts =============================================================================== platform win32 -- Python 3.12 .0 , pytest-7.0 .1 , pluggy-1.0 .0 rootdir: D:\Developer\pytest_learning\src\4. pytest fixtures explicit, modular, scalable plugins: allure-pytest-2.9 .43 , Faker-13.3 .0 , assume-2.4 .3 collected 4 items <Package 4. pytest fixtures explicit, modular, scalable> <Module test_ids.py> <Function test_a[spam]> <Function test_a[ham]> <Function test_b[eggs]> <Function test_b[1 ]
如果没有配置 ids,那么执行显示:
1 2 3 4 5 6 <Package 4.pytest fixtures explicit, modular, scalable> <Module test_ids.py> <Function test_a[0]> <Function test_a[1]> <Function test_b[0]> <Function test_b[1]>
使用 -k 执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 python -m pytest .\test_ids.py --collect-only -k "test_a[spam]" ============================================================================== test session starts =============================================================================== platform win32 -- Python 3.12.0, pytest-7.0.1, pluggy-1.0.0 rootdir: D:\Developer\pytest_learning\src\4.pytest fixtures explicit, modular, scalable plugins: allure-pytest-2.9.43, Faker-13.3.0, assume-2.4.3 collected 4 items / 3 deselected / 1 selected <Package 4.pytest fixtures explicit, modular, scalable> <Module test_ids.py> <Function test_a[spam]> ================================================================== 1/4 tests collected (3 deselected) in 0.01s =================================================================== > python -m pytest .\test_ids.py --collect-only -k "test_a[spam] or test_b[eggs]" <Package 4.pytest fixtures explicit, modular, scalable> <Module test_ids.py> <Function test_a[spam]> <Function test_b[eggs]>
对参数化固件使用标记 Using marks with parametrized fixtures
pytest.param()可在参数化固件的值集里应用于标记,与它们可用于@pytest.mark.parametrize 的方法相同 .
例子:
1 2 3 4 5 6 7 8 9 @pytest.fixture(params=[0 , 1 , pytest.param(2 , marks=pytest.mark.skip ), pytest.param(3 , marks=pytest.mark.xfail(reason="This is expected to fail" ) )] )def data_set (request ): return request.param def test_data (data_set ): assert data_set < 3
@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)]):定义了一个参数化固件 data_set,参数值为 [0, 1, pytest.param(2, marks=pytest.mark.skip)]。
pytest.param(2, marks=pytest.mark.skip):使用 pytest.param 包装参数 2,并标记为 skip。
使用 pytest.param 包装参数 3 ,标记为 xfail,测试预期失败。
1 2 3 4 5 6 7 8 9 10 11 python -m pytest .\test_fixture_marks.py -v ============================ test session starts============================== platform win32 -- Python 3.12 .0 , pytest-7.0 .1 , pluggy-1.0 .0 -- D:\software\Python\Python312\python.exe cachedir: .pytest_cache rootdir: D:\Developer\pytest_learning\src\4. pytest fixtures explicit, modular, scalable plugins: allure-pytest-2.9 .43 , Faker-13.3 .0 , assume-2.4 .3 collected 4 items test_fixture_marks.py::test_data[0 ] PASSED [ 25 %] test_fixture_marks.py::test_data[1 ] PASSED [ 50 %] test_fixture_marks.py::test_data[2 ] SKIPPED (unconditional skip) [ 75 %] test_fixture_marks.py::test_data[3 ] XFAIL (This is expected to fail)
模块化:使用fixture函数中的fixtures Modularity: using fixtures from a fixture function
不仅可以在测试函数中使用fixture,fixture函数还可以使用其他fixture本身。这有助于固件fixtures的模块化设计,并允许在许多项目中重用特定框架的固件。作为一个简单的示例,我们可以扩展前面的示例并实例化一个对象 app ,我们把已经定义好的 smtp_connection 资源加入进去了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import pytestclass App (object ): def __init__ (self, smtp_connection ): self .smtp_connection = smtp_connection @pytest.fixture(scope="module" ) def app (smtp_connection ): return App(smtp_connection) def test_smtp_connection_exists (app ): assert app.smtp_connection
我们在此声明 app 接收先前定义的 smtp_connection fixture并实例化 App 对象。让我们运行它:
1 2 3 4 5 6 7 8 9 10 11 $ pytest -v test_appsetup.py =========================== test session starts ============================ platform linux -- Python 3. x.y, pytest-4. x.y, py-1. x.y, pluggy-0. x.y -- $PYTHON_PREFIX/bin /python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 2 items test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50 %] test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100 %] ========================= 2 passed in 0.12 seconds =========================
由于参数化 smtp_connection ,测试将运行两次 App 实例和相应的SMTP服务器。 app 固件没有必要注意 smtp_connection 参数化,因为 Pytest 将充分分析固件依赖关系图。
请注意 app 固件的 module 范围,并使用 smtp_connection 模块范围固件。如果 smtp_connection被缓存在 session 范围也是可行的:fixture可以使用“更广”范围的fixture,但不能使用另一种方式:会话session范围的fixture不能以有意义的方式使用模块module范围的fixture。
按 fixture 实例自动分组测试 Automatic grouping of tests by fixture instances
在测试运行期间,pytest最小化了活跃fixtures的数量。如果你有一个参数化的fixture,那么使用它的所有测试将首先用一个实例执行,然后在创建下一个fixture实例之前调用终结器。此外,这简化了对创建和使用全局状态的应用程序的测试。
下面的示例使用两个参数化的fixture,其中一个在每个模块的基础上确定范围,所有功能都执行 print 调用以显示setup/teardown流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import pytest@pytest.fixture(scope="module" , params=["mod1" , "mod2" ] ) def modarg (request ): param = request.param print (" SETUP modarg %s" % param) yield param print (" TEARDOWN modarg %s" % param) @pytest.fixture(scope="function" , params=[1 ,2 ] ) def otherarg (request ): param = request.param print (" SETUP otherarg %s" % param) yield param print (" TEARDOWN otherarg %s" % param) def test_0 (otherarg ): print (" RUN test0 with otherarg %s" % otherarg) def test_1 (modarg ): print (" RUN test1 with modarg %s" % modarg) def test_2 (otherarg, modarg ): print (" RUN test2 with otherarg %s and modarg %s" % (otherarg, modarg))
这个示例展示了 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 31 32 33 34 35 36 37 38 39 40 41 $ pytest -v -s test_module.py =========================== test session starts ============================ platform linux -- Python 3. x.y, pytest-4. x.y, py-1. x.y, pluggy-0. x.y -- $PYTHON_PREFIX/bin /python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 8 items test_module.py::test_0[1 ] SETUP otherarg 1 RUN test0 with otherarg 1 PASSED TEARDOWN otherarg 1 test_module.py::test_0[2 ] SETUP otherarg 2 RUN test0 with otherarg 2 PASSED TEARDOWN otherarg 2 test_module.py::test_1[mod1] SETUP modarg mod1 RUN test1 with modarg mod1 PASSED test_module.py::test_2[mod1-1 ] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 PASSED TEARDOWN otherarg 1 test_module.py::test_2[mod1-2 ] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 PASSED TEARDOWN otherarg 2 test_module.py::test_1[mod2] TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 PASSED test_module.py::test_2[mod2-1 ] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 PASSED TEARDOWN otherarg 1 test_module.py::test_2[mod2-2 ] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod2 PASSED TEARDOWN otherarg 2 TEARDOWN modarg mod2 ========================= 8 passed in 0.12 seconds =========================
可以看到参数化模块作用域 modarg 资源导致测试执行的顺序,从而导致可能的“活动”资源最少。为 mod1 参数化的资源的终结器在 mod2 资源setup前被执行。
特别要注意,test_0是完全独立的,最先完成。然后是使用 mod1 的test_1执行,接着是使用 mod1的test_2 ,然后是用 mod2的test_1, 最后是用 mod2 的test_2.
这个 otherarg 参数化资源(具有函数范围)在使用它的每个测试之前setup,然后在使用它的每个测试之后teardown。
使用类、模块或项目中的 fixture Using fixtures from classes, modules or projects
有时测试函数不需要直接访问fixture对象。例如,测试可能需要使用空目录作为当前工作目录进行操作,否则不关心具体目录。以下是如何使用标准 tempfile 和pytest fixtures 来实现它。我们将创建的fixture分入conftest.py文件:
1 2 3 4 5 6 7 8 9 10 import pytestimport tempfileimport os@pytest.fixture() def cleandir (): newpath = tempfile.mkdtemp() os.chdir(newpath)
并通过一个 usefixtures 标记,在测试module中声明它的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 import osimport pytest@pytest.mark.usefixtures("cleandir" ) class TestDirectoryInit (object ): def test_cwd_starts_empty (self ): assert os.listdir(os.getcwd()) == [] with open ("myfile" , "w" ) as f: f.write("hello" ) def test_cwd_again_starts_empty (self ): assert os.listdir(os.getcwd()) == []
由于 usefixtures 标记,每个测试方法的执行都需要 cleandir fixture,就像你为每个方法指定了“cleandir”函数参数一样。让我们运行它来验证fixture是否激活,测试是否通过:
1 2 3 $ pytest -q .. [100 %] 2 passed in 0.12 seconds
可以这样指定多个fixtures:
1 2 3 @pytest.mark.usefixtures("cleandir" , "anotherfixture" ) def test (): ...
可以使用标记机制的通用特性,在测试模块级别指定fixture使用:
1 pytestmark = pytest.mark.usefixtures("cleandir" )
注意分配的变量 必须 被称为 pytestmark ,例如 foomark 不会激活fixtures。
也可以将项目中所有测试所需的fixtures放入一个ini文件中:
1 2 3 [pytest] usefixtures = cleandir
警告
注意这个标记在 fixture functions 里没有作用 . 例如,这个 无法按预期工作 :
1 2 3 4 @pytest.mark.usefixtures("my_other_fixture" ) @pytest.fixture def my_fixture_that_sadly_wont_use_my_other_fixture (): ...
目前,这不会产生任何错误或警告.
自动固定装置 autouse Autouse fixtures (xUnit setup on steroids)
有时,你可能希望在不显式声明函数参数或 usefixtures 装饰器的情况下自动调用fixtures。作为一个实际的例子,假设我们有一个数据库设备,它有一个 begin/rollback/commit 体系结构,并且我们希望通过一个 transaction 和一个 rollback 自动包围每个测试方法。下面是这个想法的一个虚拟的独立实现:
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 import pytestclass DB (object ): def __init__ (self ): self .intransaction = [] def begin (self, name ): self .intransaction.append(name) def rollback (self ): self .intransaction.pop() @pytest.fixture(scope="module" ) def db (): return DB() class TestClass (object ): @pytest.fixture(autouse=True ) def transact (self, request, db ): db.begin(request.function.__name__) yield db.rollback() def test_method1 (self, db ): assert db.intransaction == ["test_method1" ] def test_method2 (self, db ): assert db.intransaction == ["test_method2" ]
类级别 transact 固件标有 autouse=true 这意味着类中的所有测试方法都将使用这个fixture,而不需要在测试函数签名或类级别的usefixture装饰器中声明它。
如果我们运行它,我们会得到两个通过的测试:
1 2 3 $ pytest -q .. [100 %] 2 passed in 0.12 seconds
以下是Autouse 固件在其他范围中的工作方式:
autouse fixtures遵守 scope= 关键字参数:如果一个autouse fixture具有 scope='session' ,无论它在何处被定义,它将只运行一次。而 scope='class' 意味着它将每个类运行一次,等等。
如果在一个测试模块中定义了一个autouse fixture,那么它的所有测试函数都会自动使用它。
如果在conftest.py文件中定义了一个autouse fixture,那么其目录下的所有测试模块中的所有测试都将调用该fixture。
最后, 请小心使用 :如果你在一个插件中定义了一个autouse fixture,它将对安装该插件的所有项目中的所有测试进行调用。如果一个fixture只在某些设置(例如在ini文件中)下工作,这是有用的。这样一个全局fixture应该总是快速确定它是否应该做任何工作,并避免其他耗费的导入或计算。
注意上面 transact fixture很可能是你希望在项目中可用的一个fixture,而通常不需要它处于活动状态。实现这一点的规范方法是将transact的定义放入没有使用 autouse 的conftest.py 中:
1 2 3 4 5 6 @pytest.fixture def transact (request, db ): db.begin() yield db.rollback()
然后,例如,通过声明需求,让一个testclass使用它:
1 2 3 4 @pytest.mark.usefixtures("transact" ) class TestClass (object ): def test_method1 (self ): ...
此TestClass中的所有测试方法都将使用transact fixture,而模块中的其他测试类或函数将不使用它,除非它们还添加了 transact 参考。
覆盖不同级别的设备 Overriding fixtures on various levels
在相对较大的测试套中,你很可能需要 override 一个 global 或 root fixture与 locally 定义一个,用来保持测试代码的可读性和可维护性。
覆盖文件夹(conftest)级别的fixture Override a fixture on a folder (conftest) level
假设测试文件结构为:
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 tests/ __init__.py conftest.py import pytest @pytest.fixture def username (): return 'username' test_something.py def test_username (username ): assert username == 'username' subfolder/ __init__.py conftest.py import pytest @pytest.fixture def username (username ): return 'overridden-' + username test_something.py def test_username (username ): assert username == 'overridden-username'
以上示例,具有相同名称的fixture可以被某些测试文件夹级别的覆盖。请注意 base 或 super fixture可从 在上面的例子中很容易使用的overriding fixture中获取。
在测试模块级别上覆盖fixture Override a fixture on a test module level
假设测试文件结构为:
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 tests/ __init__.py conftest.py @pytest.fixture def username (): return 'username' test_something.py import pytest @pytest.fixture def username (username ): return 'overridden-' + username def test_username (username ): assert username == 'overridden-username' test_something_else.py import pytest @pytest.fixture def username (username ): return 'overridden-else-' + username def test_username (username ): assert username == 'overridden-else-username'
在上面的示例中,一个具有相同名称fixture可以为某些测试模块重写覆盖。
通过直接测试参数化覆盖夹具 Override a fixture with direct test parametrization
假设测试文件结构为:
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 tests/ __init__.py conftest.py import pytest @pytest.fixture def username (): return 'username' @pytest.fixture def other_username (username ): return 'other-' + username test_something.py import pytest @pytest.mark.parametrize('username' , ['directly-overridden-username' ] ) def test_username (username ): assert username == 'directly-overridden-username' @pytest.mark.parametrize('username' , ['directly-overridden-username-other' ] ) def test_username_other (other_username ): assert other_username == 'other-directly-overridden-username-other'
在上面的示例中,fixture值被测试参数值覆盖。注意,即使测试没有直接使用fixture的值(在函数原型中没有提到),也可以用这种方式覆盖fixture的值。
用非参数化固件替代参数化固件 Override a parametrized fixture with non-parametrized one and vice versa
假设测试文件结构为:
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 tests/ __init__.py conftest.py import pytest @pytest.fixture(params=['one' , 'two' , 'three' ] ) def parametrized_username (request ): return request.param @pytest.fixture def non_parametrized_username (request ): return 'username' test_something.py import pytest @pytest.fixture def parametrized_username (): return 'overridden-username' @pytest.fixture(params=['one' , 'two' , 'three' ] ) def non_parametrized_username (request ): return request.param def test_username (parametrized_username ): assert parametrized_username == 'overridden-username' def test_parametrized_username (non_parametrized_username ): assert non_parametrized_username in ['one' , 'two' , 'three' ] test_something_else.py def test_username (parametrized_username ): assert parametrized_username in ['one' , 'two' , 'three' ] def test_username (non_parametrized_username ): assert non_parametrized_username == 'username'
在上面的示例中,参数化固件被非参数化版本覆盖,而非参数化固件被某些测试模块的参数化版本覆盖。显然,这同样适用于测试文件夹级别。