四、关于fixtures测试夹具的使用【重点】 这篇文章主要介绍fixture
夹具,在项目中的使用技巧,代码比较多,文章比较长,请耐心看完
有些官方文档上的代码比价绕或者冗余,就自己写了简单版的便于理解
4.1 “Requesting” fixtures(请求夹具) 在基本层面上,测试函数通过将它们声明为参数来请求它们所需的装置。
当pytest运行测试时,它会查看该测试函数签名中的参数,然后搜索与这些参数名称相同的fixture。一旦pytest找到它们,它就运行这些fixture,捕获它们返回的内容(如果有的话),并将这些对象作为参数传递给test函数。
4.1.1 快速示例Quick example 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 import pytestclass Fruit : def __init__ (self, name ): self.name = name self.cubed = False def cube (self ): self.cubed = True class FruitSalad : def __init__ (self, *fruit_bowl ): self.fruit = fruit_bowl self._cube_fruit() def _cube_fruit (self ): for fruit in self.fruit: fruit.cube() @pytest.fixture def fruit_bowl (): return [Fruit("apple" ), Fruit("banana" )] def test_fruit_salad (fruit_bowl ): fruit_salad = FruitSalad(*fruit_bowl) assert all (fruit.cubed for fruit in fruit_salad.fruit)
如果要手动完成,需要这么做:
1 2 3 4 5 6 7 8 9 10 11 12 13 ef fruit_bowl(): return [Fruit("apple" ), Fruit("banana" )]def test_fruit_salad (fruit_bowl ): fruit_salad = FruitSalad(*fruit_bowl) assert all (fruit.cubed for fruit in fruit_salad.fruit) bowl = fruit_bowl() test_fruit_salad(fruit_bowl=bowl)
在这个例子中,test_fruit_salad“请求”了fruit_bowl(即def test_fruit_salad(fruit_bowl):),当pytest看到这个时,它将执行fruit_bowl固定函数,并将它返回的对象作为fruit_bowl参数传递给test_fruit_salad。
官方的这里示例描述的不太好,会把人看晕,看下面的就很简单明了,官方想表达的是,你如何定义一个fixture函数,然后如何使用它,你只需要在测试函数的入参里面加入即可,不需要手动执行你定义的fixture函数foo( )
1 2 3 4 5 6 7 import pytest@pytest.fixture def foo (): return 1 def test_foo (foo ): a = foo assert a == 1
4.1.2 Fixtures 也可以请求其他 fixtures【重点】 pytest最大的优势之一是其极其灵活的夹具系统。它允许我们将复杂的测试需求简化为更简单和有组织的功能,我们只需要让每个功能描述它们所依赖的东西。我们将在后面深入讨论这个问题,但是现在,这里有一个快速的示例来演示fixture如何使用其他fixture:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import pytest@pytest.fixture def first_entry (): return "a" @pytest.fixture def order (first_entry ): return [first_entry]def test_string (order ): order.append("b" ) assert order == ["a" , "b" ]
如果手动调用就是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 def first_entry (): return "a" def order (first_entry ): return [first_entry]def test_string (order ): order.append("b" ) assert order == ["a" , "b" ] entry = first_entry() the_list = order(first_entry=entry) test_string(order=the_list)
4.1.3 Fixtures 可以重复使用【重点】 pytest
的fixture
系统如此强大的原因之一就是,它使我们能够定义一个可以重复使用的通用设置步骤 ,就像使用普通函数一样。两个不同的测试可以请求相同的fixture,并让pytest
从该fixture
为每个测试提供各自的结果。
这对于确保测试用例不会相互影响非常有用。我们可以使用这个系统来确保每个测试都获得自己的一批新鲜数据,并且从干净的状态开始,这样它就可以提供一致、可重复的结果。
使用的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import pytest@pytest.fixture def first_entry (): return "a" @pytest.fixture def order (first_entry ): return [first_entry]def test_string (order ): order.append("b" ) assert order == ["a" , "b" ]def test_int (order ): order.append(2 ) assert order == ["a" , 2 ]
这里的每个测试用例都被赋予了该列表对象的自己的副本,这意味着fixture
装置函数被执行两次(对于first_entry
固定装置也是如此)。
如果我们也手动执行此操作,就是这样写的:
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 def first_entry (): return "a" def order (first_entry ): return [first_entry]def test_string (order ): order.append("b" ) assert order == ["a" , "b" ]def test_int (order ): order.append(2 ) assert order == ["a" , 2 ] entry = first_entry() the_list = order(first_entry=entry) test_string(order=the_list) entry = first_entry() the_list = order(first_entry=entry) test_int(order=the_list)
4.1.4 一个 test或者fixture可以同时请求多个fixture【重点】 测试用例和fixture装置不限于一次请求一个fixture装置。他们可以要求任意数量的。
这是另一个演示的简单示例:
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 import pytest@pytest.fixture def first_entry (): return "a" @pytest.fixture def second_entry (): return 2 @pytest.fixture def order (first_entry, second_entry ): return [first_entry, second_entry] @pytest.fixture def expected_list (): return ["a" , 2 , 3.0 ]def test_string (order, expected_list ): order.append(3.0 ) assert order == expected_list
这里讲的几个还是比较重要的
1、如何使用fixture
4.1.5 每次测试可以多次请求fixture夹具(返回值被缓存) 在同一测试用例期间也可以多次请求fixture夹具函数,、
并且 pytest 不会在该测试中再次执行它们,
意味着我们可以在依赖于它们的多个fixture装置中请求其他fixture装置(甚至在测试本身中再次请求fixture装置),而无需多次执行这些fixture装置。—-看着有点绕先不管,接着往下看
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 import pytest@pytest.fixture def first_entry (): print (f"如果运行了{first_entry.__name__} 就会打印这句话" ) return "a" @pytest.fixture def order (): print (f"如果运行了{order.__name__} 就会打印这句话" ) return []@pytest.fixture def append_first (order, first_entry ): print (f"如果运行了{append_first.__name__} 就会打印这句话" ) return order.append(first_entry)def test_string_only (append_first, order, first_entry ): print (order) print (first_entry) assert order == [first_entry]
如果在测试期间每次请求时都执行一次所定义的fixture装置的话 ,那么这个测试就会失败,、
因为append_first
方法和test_string_only
用例都会将order
视为一个空列表(如[ ]),
但由于 order
的返回值在第一次调用后被缓存(以及执行它可能产生的任何副作用),测试用例判断了append_first
对该对象的影响。
这里官方想表达的是,fixture在fixture和test_之间混合使用不会重复执行,看一下终端输出结果会更清晰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ================================================================================ test session starts ================================================================================ platform linux -- Python 3.10 .12 , pytest-8.1 .1 , pluggy-1.4 .0 plugins: anyio-4.2 .0 collected 1 item testcases/test_fixturestest.py 如果运行了order就会打印这句话 如果运行了first_entry就会打印这句话 如果运行了append_first就会打印这句话 ['a' ] a . ================================================================================= 1 passed in 0.01 s =================================================================================
这里的每段print( xxx ),都只被打印了一次,如果会重复执行的话,就会打印多次了。
4.2 自动使用fixtue(不必主动请求的fixture) 测试中有时可能想要有一个(甚至几个)让所有测试用例都依赖的fixture
装置,
自动使用fixture
装置是一种让所有测试用例都自动请求它们的便捷方法。这可以消除大量冗余的请求,甚至可以提供更高级的夹具使用(更多内容请参见下文)
可以通过将 autouse=True
传递给装饰器pytest.fixture
来使fixture
成为自动使用的fixture
。
代码简单示例:
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 pytest@pytest.fixture def first_entry (): return "a" @pytest.fixture def order (first_entry ): return []@pytest.fixture(autouse=True ) def append_first (order, first_entry ): return order.append(first_entry)def test_string_only (order, first_entry ): print (order) assert order == [first_entry]def test_string_and_int (order, first_entry ): order.append(2 ) print (order) assert order == [first_entry, 2 ]
在此示例中,append_first
固定装置是自动使用固定装置。因为它是自动发生的,所以两个测试都会受到它的影响,即使两个测试都没有请求它。但这并不意味着他们不能被请求;只是没有必要写罢了—->隐式传递
4.3 作用域:跨类、模块、包或会话共享fixture【重点】 4.3.1理解fixture 作用域【重点】 需要网络访问的fixtures
取决于连接性,并且创建起来通常非常耗时。
扩展前面的例子
我们可以在 @pytest.fixture
调用中添加一个 scope="module"
参数来引发 smtp_connection 固定功能,负责创建到预先存在的 SMTP 服务器的连接,
每个测试模块仅调用一次(默认为每个测试函数调用一次)。
因此,测试模块中的多个测试函数将各自接收相同的 smtp_connection 夹具实例,从而节省时间
范围的可能值为:函数、类、模块、包或会话。
下一个示例将fixture函数放入单独的conftest.py文件中,以便目录中多个测试模块的测试可以访问fixture函数:
官方的代码有点看的不是特别清晰,可以直接看下面代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import smtplibimport pytest@pytest.fixture(scope="module" ) def smtp_connection (): return smtplib.SMTP("smtp.gmail.com" , 587 , timeout=5 )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
这里,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 23 24 25 26 27 28 29 30 31 32 33 34 35 36 $ pytest test_module.py =========================== test session starts ============================ platform linux -- Python 3. x.y, pytest-8. x.y, pluggy-1. x.y rootdir: /home/sweet/project collected 2 items test_module.py FF [100 %] ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001 > 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:7 : AssertionError ________________________________ test_noop _________________________________ smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001 > def test_noop (smtp_connection ): response, msg = smtp_connection.noop() assert response == 250 > assert 0 E assert 0 test_module.py:13 : AssertionError ========================= short test summary info ========================== FAILED test_module.py::test_ehlo - assert 0 FAILED test_module.py::test_noop - assert 0 ============================ 2 failed in 0.12 s =============================
您会看到两个断言 0 失败,更重要的是您还可以看到完全相同的 smtp_connection 对象被传递到两个测试函数中,因为 pytest 在回溯中显示传入的参数值。因此,使用 smtp_connection 的两个测试函数运行速度与单个测试函数一样快,因为它们重复使用相同的实例。
两个测试用例,虽然都引入的这个fixture,但是,其实这个fixture是只运行了一次,没有被重复的运行,而且只在模块级别运行,假如这个fixture是函数级别的,那就是每个测试方法单独运行一次,同理,假如是类级别的,那就是每个类单独运行一次,下面会介绍所有级别
看下面的代码可以更清晰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class MyClass : """这个类,如果是第一次被实例化对象,age的值就是1,如果是第二次,age得值就是2""" instance_count = 0 def __init__ (self ): MyClass.instance_count += 1 self.age = MyClass.instance_count@pytest.fixture(scope="module" ) def module_test (): Tom = MyClass() return Tom def test_ehlo (module_test ): TestTom = module_test assert TestTom.age == 1 def test_noop (module_test ): TestTom = module_test assert TestTom.age == 1
如果改一下,改成function 级别的呢?测试用例就不会通过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class MyClass : """这个类,如果是第一次被实例化对象,age的值就是1,如果是第二次,age得值就是2""" instance_count = 0 def __init__ (self ): MyClass.instance_count += 1 self.age = MyClass.instance_count@pytest.fixture(scope="function" ) def module_test (): Tom = MyClass() return Tom def test_ehlo (module_test ): TestTom = module_test assert TestTom.age == 1 def test_noop (module_test ): TestTom = module_test assert TestTom.age == 1
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ================================ FAILURES ================================================= ________________________________ test_noop _________________________________________ module_test = <test_fixturestest.MyClass object at 0x7f0eeb976b60 > def test_noop (module_test ): TestTom = module_test > assert TestTom.age == 1 E assert 2 == 1 E + where 2 = <test_fixturestest.MyClass object at 0x7f0eeb976b60 >.age testcases/test_fixturestest.py:206 : AssertionError ======================= short test summary info =========================================== FAILED testcases/test_fixturestest.py::test_noop - assert 2 == 1 ============================ 1 failed, 1 passed in 0.01 s ==============================
如果您决定希望拥有一个会话范围的 smtp_connection 实例,您可以简单地声明它:
1 2 3 4 5 @pytest.fixture(scope="session" ) def smtp_connection (): ...
4.3.2 Fixture 作用域范围【重点】 夹具在测试首次请求时创建,并根据其范围销毁:
function
: 默认范围,fixture在每个测试用例结束时销毁。.
class
: 该fixture在class最后一次测试的测试用例时被销毁。
module
: 模块级别(py文件)
package
: 包级别(文件夹)
session
: 会话级别(整场测试)
模块级别、包级别、会话级别也是一样,因为fixture是在conftest.py文件里写的,这个文件是放在项目的根目录下的,你的测试用例,不一定全都是放在一个模块内(py文件)一个包内(文件夹),会话级别的意思就是整场测试用例的级别。
Pytest 一次仅缓存fixture装置的一个实例,这意味着当使用参数化fixture装置时,pytest 可能会在给定范围内多次调用fixture装置。
4.3.3 fixture 动态范围–自定义作用域 在某些情况下,您可能希望在不更改代码的情况下更改夹具的范围。为此,请将可调用对象传递给作用域。
可调用函数必须返回一个具有有效范围的字符串,并且仅在夹具定义期间执行一次。它将使用两个关键字参数进行调用——fixture_name 作为字符串,config 使用配置对象。
当处理需要时间设置的固定装置(例如生成 docker 容器)时,这尤其有用。
可以使用命令行参数来控制不同环境下生成的容器的范围。请参阅下面的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 def determine_scope (fixture_name, config ): if config.getoption("--keep-containers" , None ): return "session" return "function" @pytest.fixture(scope=determine_scope ) def docker_container (): yield spawn_container()
4.4 Teardown/Cleanup (又称前置和后置) 当我们运行测试时,我们需要确保它们自行清理,这样它们就不会干扰任何其他测试(同时我们也不会留下大量的测试数据来使系统膨胀)
pytest 中的装置提供了一个非常有用的拆卸系统,它允许我们定义每个装置自行清理所需的特定步骤。
可以通过两种方式利用该系统。
1. yield
fixtures 【推荐】 “Yield” fixtures使用 yield
而不是 return
.,我们可以运行一些代码并将对象传递回请求的装置/测试,就像其他装置一样。唯一的区别是:
return
换成yield
一些 teardown
代码放在yiend
的后面
一旦 pytest 计算出fixtures装置的线性顺序,它将运行每个fixtures装置,直到它返回或产生,然后转到列表中的下一个fixtures装置以执行相同的操作。
测试完成后,pytest 将返回fixtures装置列表,但以相反的顺序,获取每个生成的fixtures装置,并运行其中位于yield 语句之后的代码。
举一个例子:
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 class MailAdminClient : def create_user (self ): return MailUser() def delete_user (self, user ): pass class MailUser : def __init__ (self ): self.inbox = [] def send_email (self, email, other ): other.inbox.append(email) def clear_mailbox (self ): self.inbox.clear()class Email : def __init__ (self, subject, body ): self.subject = subject self.body = body
假设我们要测试从一个用户向另一个用户发送电子邮件。
我们必须首先创建每个用户,
然后将电子邮件从一个用户发送到另一个用户,
最后断言另一个用户在其收件箱中收到了该消息。
如果我们想在测试运行后进行清理,我们可能必须确保在删除其他用户之前清空该用户的邮箱,否则系统可能会有垃圾数据。
看一下示例:
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 54 class MailAdminClient : def create_user (self ): print ("create user" ) return MailUser() def delete_user (self, user ): print ("delete user" ) pass class MailUser : def __init__ (self ): self.inbox = [] def send_email (self, email, other ): print ("send email" ) other.inbox.append(email) def clear_mailbox (self ): print ("clearing mailbox" ) self.inbox.clear()class Email : def __init__ (self, subject, body ): self.subject = subject self.body = body@pytest.fixture def mail_admin (): return MailAdminClient()@pytest.fixture def sending_user (mail_admin ): user = mail_admin.create_user() yield user mail_admin.delete_user(user)@pytest.fixture def receiving_user (mail_admin ): user = mail_admin.create_user() yield user user.clear_mailbox() mail_admin.delete_user(user)def test_email_received (sending_user, receiving_user ): email = Email(subject="Hey!" , body="How's it going?" ) sending_user.send_email(email, receiving_user) assert email in receiving_user.inbox
看一下运行的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ============================ test session starts ======================================= platform linux -- Python 3.10 .12 , pytest-8.1 .1 , pluggy-1.4 .0 plugins: anyio-4.2 .0 collected 1 item testcases/test_fixturestest.py create user create user send email .clearing mailbox delete user delete user ========================= 1 passed in 0.01 s =============================================
因为receiving_user是安装过程中最后一个运行的fixture装置,所以它也是拆卸过程中第一个运行的fixture装置。
yield fixture处理错误 如果yield装置在yield之前引发异常,pytest将不会尝试在该yield装置的yield语句之后运行结束代码。
但是,对于已经成功运行该测试的每个fixture装置,pytest 仍会像平常一样尝试将其拆除。
也就是说,如果你在yield之前报错了,就不会运行yield之后的代码了,如果yield之前没报错,就肯定会运行yield之后的代码,哪怕yield之后也有报错
2. 直接添加终结器finalizers 虽然yield
夹具被认为是更简单、更直接的选项,但还有另一种选择,那就是将“finalizers”函数直接添加到测试的请求上下文对象request
中。
它带来了与yield装置类似的结果,但可能会让代码变得冗余了一点。
为了使用这种方法,我们必须在需要添加拆卸代码的固定装置中请求请求上下文对象(就像我们请求另一个固定装置一样),然后将包含该拆卸代码的可调用对象传递给其 addfinalizer 方法。
但我们必须小心,因为 pytest 在添加finalizers
后将运行该终结器,即使该装置在添加finalizers
后引发异常
因此,为了确保我们在不需要时不运行终结器代码,只有在夹具完成了我们需要拆卸的操作后,我们才会添加终结器。
这里的目的是,你可以将清理用户数据的方法,放在这个用例执行完成后执行,以下是使用 addfinalizer 方法的示例:
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 from emaillib import Email, MailAdminClientimport pytest@pytest.fixture def mail_admin (): return MailAdminClient()@pytest.fixture def sending_user (mail_admin ): user = mail_admin.create_user() yield user mail_admin.delete_user(user)@pytest.fixture def receiving_user (mail_admin, request ): user = mail_admin.create_user() def delete_user (): mail_admin.delete_user(user) request.addfinalizer(delete_user) return user@pytest.fixture def email (sending_user, receiving_user, request ): _email = Email(subject="Hey!" , body="How's it going?" ) sending_user.send_email(_email, receiving_user) def empty_mailbox (): receiving_user.clear_mailbox() request.addfinalizer(empty_mailbox) return _emaildef test_email_received (receiving_user, email ): assert email in receiving_user.inbox
执行的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ====================== test session starts ============================ platform linux -- Python 3.10 .12 , pytest-8.1 .1 , pluggy-1.4 .0 configfile: pytest.ini plugins: anyio-4.2 .0 collected 1 item testcases/test_fixturestest.py create user create user send email .clearing mailbox delete user delete user ======================================= 1 passed in 0.01 s ==========
request.addfinalizer()
添加的方法会在当前测试用例执行完成后运行。它会在测试用例的所有代码(包括测试函数、fixture、setup 和 teardown)执行完毕后被调用。
具体来说,request.addfinalizer()
方法用于注册一个清理函数,该函数将在以下情况之一发生时被调用:
当前测试用例的测试函数执行完成后。
当前测试用例中的任何 fixture 的 yield
语句之后。
当前测试用例中的任何 fixture 的 teardown
方法执行完成后。
简化一下代码
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 @pytest.fixture def my_fixture (request ): def cleanup (): print ("Performing cleanup,这是最后执行的" ) request.addfinalizer(cleanup) print ("Setup" ) yield print ("Teardown" )def test_example2 (my_fixture ): print ("Running test" ) @pytest.fixture def my_fixture (request ): request.addfinalizer(lambda : print ("Performing cleanup,这是最后执行的" )) print ("Setup" ) yield print ("Teardown" )def cleanup (): print ("Performing cleanup,这是最后执行的" )def my_fixture (request ): request.addfinalizer(cleanup) print ("Setup" ) yield print ("Teardown" )
执行的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ====================== test session starts ================================= platform linux -- Python 3.10 .12 , pytest-8.1 .1 , pluggy-1.4 .0 configfile: pytest.ini plugins: anyio-4.2 .0 collected 1 item testcases/test_fixturestest.py Setup Running test .Teardown Performing cleanup,这是最后执行的 ======================== 1 passed in 0.01 s =========
request.addfinalizer()
和fixture
函数里面添加yield
的区别在于:
request.addfinalizer()
的函数是在测试结束后执行,
request.addfinalizer()
可以在程序发生报错后仍然执行,而使用yield
的话,yield
前面的代码在执行时如果报错,那yield
后面的代码就不会执行,
另关于对于request
的使用,后续开一篇文章讲解
关于finalizer顺序的注意事项【重点】 finalizer
按照先进后出的顺序执行。对于yield
的fixture
,要运行的第一个teardown
代码来自最右侧的fixture
装置,即最后一个测试参数(入参)。
看代码
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 import pytestdef test_bar (fix_w_yield1, fix_w_yield2 ): print ("test_bar" )@pytest.fixture def fix_w_yield1 (): print ("after_yield_1,start" ) yield print ("after_yield_1,end" )@pytest.fixture def fix_w_yield2 (): print ("after_yield_2,start" ) yield print ("after_yield_2,end" ) $ pytest -s test_finalizers.py =========================== test session starts ============================ platform linux -- Python 3. x.y, pytest-8. x.y, pluggy-1. x.y rootdir: /home/sweet/project collected 1 item test_finalizers.py test_bar .after_yield_2 after_yield_1 yield_1,start after_yield_2,start test_bar .after_yield_2,end after_yield_1,end ============================ 1 passed in 0.12 s =============================
对于finalizers内注册的函数,优先运行最后一个添加进去的函数,依次往前运行。代码示例如下:
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 from functools import partialimport pytest@pytest.fixture def fix_w_finalizers (request ): request.addfinalizer(partial(print , "finalizer_2" )) request.addfinalizer(partial(print , "finalizer_1" )) def test_bar (fix_w_finalizers ): print ("test_bar" ) $ pytest -s test_finalizers.py =========================== test session starts ============================ platform linux -- Python 3. x.y, pytest-8. x.y, pluggy-1. x.y rootdir: /home/sweet/project collected 1 item test_finalizers.py test_bar .finalizer_1 finalizer_2 ============================ 1 passed in 0.12 s =============================
之所以如此,是因为产量yield fixtures装置在幕后使用了 addfinalizer:当fixture装置执行时,addfinalizer 注册一个恢复生成器的函数,该函数又调用teardown代码。
所以一个测试用例内的fixture的执行顺序是,入参中的最后一个先执行,addfinalizer注册的函数最后执行
4.5 teardowns安全【注意】 pytest 的固定系统非常强大,但它仍然由计算机运行,因此它无法弄清楚如何安全地拆除我们扔给它的所有东西
如果我们不小心,错误位置的错误可能会留下测试中的内容,这可能很快会导致进一步的问题。
例如,考虑以下测试(基于上面的邮件示例):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from emaillib import Email, MailAdminClientimport pytest@pytest.fixture def setup (): mail_admin = MailAdminClient() sending_user = mail_admin.create_user() receiving_user = mail_admin.create_user() email = Email(subject="Hey!" , body="How's it going?" ) sending_user.send_email(email, receiving_user) yield receiving_user, email receiving_user.clear_mailbox() mail_admin.delete_user(sending_user) mail_admin.delete_user(receiving_user)def test_email_received (setup ): receiving_user, email = setup assert email in receiving_user.inbox
这个版本的代码看起来确实是更加紧凑,只定义了一个fixture
函数,但是可这样的代码读性也会更差,没有什么描述性的fixture
名称,一点也不见名知意,并且也无法做到轻松的重复使用这个fixture
,复用性差。
这里还有一个更严重的问题,即如果在运行代码的时候有任何步骤引发了程序异常报错,则任何代码都不会运行。
所以有另一种选择就是使用 addfinalizer 注册方法而不是全部都堆在fixture里面,但是用addfinalizer的话可能会是代码变得相当复杂且难以维护(并且它不再紧凑)。有利有弊吧
4.5.1 fixture 安全结构 最安全和最简单的fixture
结构要求限制一个fixture
仅进行一个状态的更改操作(意思就是一个fixture
只做一件事情,逻辑不可以写多了 ),
然后将多个fixture
与其他fixture
代码捆绑在一起,如上面的电子邮件示例所示。
状态更改操作失败但仍然修改状态的可能性可以忽略不计,因为大多数这些操作往往是基于事务的(至少在状态可能被抛在后面的测试级别)。
因此,如果我们通过将任何成功的状态更改操作移动到单独的fixture
并将其与其他有可能失败的状态更改操作分开来确保将其最后执行,那么我们的测试将有最多的可能离开测试环境
这里的意思,将必定成功的操作,和可能失败的操作拆分开来,将成功率大的和成功率大的操作放在一起,失败率大的放一起或者拆开来放,避免一头成功一头失败的,造成其他安全隐患
举个例子,假设我们有一个带有登录页面的网站,并且我们可以访问可以生成用户的管理 API。 对于我们的测试,我们想要:
通过该API 创建管理用户
使用 Selenium 启动浏览器
转到我们网站的登录页面
以我们创建的用户身份登录
在登录的标题中断言我们的用户名称
我们不想让该用户留在系统数据中,避免脏数据,也不想让浏览器会话保持运行,因此我们希望确保创建这些东西的装置能够自行清理脏数据和关闭浏览器页面。
示例看起来是这样的:
对于此示例,某些固定装置(即 base_url 和 admin_credentials)暗示存在于其他地方。所以现在,我们假设它们存在,但我们只是不去关注它们。
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 from uuid import uuid4from urllib.parse import urljoinfrom selenium.webdriver import Chromeimport pytestfrom src.utils.pages import LoginPage, LandingPagefrom src.utils import AdminApiClientfrom src.utils.data_types import User@pytest.fixture def admin_client (base_url, admin_credentials ): return AdminApiClient(base_url, **admin_credentials)@pytest.fixture def user (admin_client ): _user = User(name="Susan" , username=f"testuser-{uuid4()} " , password="P4$$word" ) admin_client.create_user(_user) yield _user admin_client.delete_user(_user)@pytest.fixture def driver (): _driver = Chrome() yield _driver _driver.quit()@pytest.fixture def login (driver, base_url, user ): driver.get(urljoin(base_url, "/login" )) page = LoginPage(driver) page.login(user)@pytest.fixture def landing_page (driver, login ): return LandingPage(driver)def test_name_on_landing_page_after_login (landing_page, user ): assert landing_page.header == f"Welcome, {user.name} !"
依赖关系的布置方式意味着不清楚用户fixture
是否会在驱动程序fixture
之前执行。
但这没关系,因为这些都是原子操作,所以哪个先运行并不重要,因为测试的事件序列仍然是线性化的。
但重要的是,无论哪一个先运行,如果某一个抛出异常,而另一个没有抛出异常,那么两个都不会留下任何东西。
如果驱动程序fixture
在用户fixture
之前执行,并且用户fixture
引发异常,驱动程序fixture
仍然会退出,并且用户永远不会被创建。 如果驱动程序是引发异常的人,那么驱动程序将永远不会启动,用户也永远不会被创建。
这里的意思还是做拆分,就像代码分层一样,原子层的操作和业务层的操作分开来,这里的代码就是将登录的操作和创建用户的操作分了层,打开浏览器的操作
admin_client(base_url, admin_credentials) :这个fixture只负责连接对应的 Url
user(admin_client):这个fixture会使用到admin_client,只负责创建管理员用户,和结束时删除用户
driver() :这个fixture,只负责定义浏览器驱动打开和关闭浏览器,不做其他操作
login(driver, base_url, user): 这个fixute是一个组合,将driver 和 base_url 和 user 三个组合到一起,作用是打开浏览器、打开对应的url 执行用户操作,结束时会将所有fixtue的结束操作全部执行
这是很好的一种编码设计思想,一种分层的思想,将用户登录的操作全部分层出来,测试代码只负责最终的组合,不关注它所依赖的fixture 的实现逻辑,只关注进和出。这里很重要
4.6 安全的运行多个assert
断言语句 有时,您可能希望在完成所有设置后运行多个断言,这是有道理的,因为在更复杂的系统中,单个操作可以启动多个行为
pytest 有一种方便的方法来处理这个问题,它结合了我们到目前为止所讨论的一些内容。
所需要的只是逐步扩大到更大的范围,然后将行为步骤定义为自动使用固定装置,最后确保所有固定装置都针对更高级别的范围
让我们从上面举一个例子,并稍微调整一下。假设除了检查标题中的欢迎消息之外,我们还想检查注销按钮和用户个人资料的链接。
让我们看一下如何构建它,以便我们可以运行多个断言,而不必再次重复所有这些步骤。
对于此示例,某些固定装置(即 base_url 和 admin_credentials)暗示存在于其他地方。所以现在,我们假设它们存在,但我们只是不去关注它们。
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 from uuid import uuid4from urllib.parse import urljoinfrom selenium.webdriver import Chromeimport pytestfrom src.utils.pages import LoginPage, LandingPagefrom src.utils import AdminApiClientfrom src.utils.data_types import User@pytest.fixture(scope="class" ) def admin_client (base_url, admin_credentials ): return AdminApiClient(base_url, **admin_credentials)@pytest.fixture(scope="class" ) def user (admin_client ): _user = User(name="Susan" , username=f"testuser-{uuid4()} " , password="P4$$word" ) admin_client.create_user(_user) yield _user admin_client.delete_user(_user)@pytest.fixture(scope="class" ) def driver (): _driver = Chrome() yield _driver _driver.quit()@pytest.fixture(scope="class" ) def landing_page (driver, login ): return LandingPage(driver)class TestLandingPageSuccess : @pytest.fixture(scope="class" , autouse=True ) def login (self, driver, base_url, user ): driver.get(urljoin(base_url, "/login" )) page = LoginPage(driver) page.login(user) def test_name_in_header (self, landing_page, user ): assert landing_page.header == f"Welcome, {user.name} !" def test_sign_out_button (self, landing_page ): assert landing_page.sign_out_button.is_displayed() def test_profile_link (self, landing_page, user ): profile_href = urljoin(base_url, f"/profile?id={user.profile_id} " ) assert landing_page.profile_link.get_attribute("href" ) == profile_href
请注意,这些方法只是在签名中引用 self 作为一种形式。没有任何状态与实际的测试类相关,因为它可能位于 unittest.TestCase 框架中。一切都由 pytest 夹具系统管理。
每个方法只需要请求它实际需要的fixture,而不用担心执行顺序。这是因为fixture装置是一个自动使用的fixture,并且它确保所有其他fixture在它之前执行。
不再需要进行状态更改,因此测试可以自由地进行任意数量的非状态更改查询,而不必冒踩到其他测试的风险。
登录装置也在类内部定义,因为并非模块中的每个其他测试都期望成功登录,并且对于另一个测试类,该行为可能需要稍微不同地处理。可以每个类每个登录
这里表达的是,在一个测试类内,运行多个测试用例,多个断言,不会因为某个测试用例的断言失败了导致另一个测试用例无法运行下下去,虽然他们都是使用的同一个fixture夹具,你需要其他的登录前置,那么你可以在另一个类内编写一个fixture 供这个类内的用例去使用,
例如,如果我们想围绕提交错误凭据编写另一个测试场景,我们可以通过在测试文件中添加如下内容来处理它:
1 2 3 4 5 6 7 8 9 10 class TestLandingPageBadCredentials : @pytest.fixture(scope="class" ) def faux_user (self, user ): _user = deepcopy(user) _user.password = "badpass" return _user def test_raises_bad_credentials_exception (self, login_page, faux_user ): with pytest.raises(BadCredentialsException): login_page.login(faux_user)
4.7 Fixtures 可以使用内置的requesting来测试上下文【重点】 fixture
函数可以接受requesting
对象来内省“request”来测试函数、类或模块上下文。
进一步扩展前面的 smtp_connection 夹具示例,让我们从使用我们的夹具的测试模块中读取可选的服务器 URL:
1 2 3 4 5 6 7 8 9 10 11 12 import smtplibimport pytest@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 (f"finalizing {smtp_connection} ({server} )" ) smtp_connection.close()
我们使用request.module
属性来选择性地从测试模块获取 smtpserver
属性。如果我们再次执行,没有什么改变:
1 2 3 4 5 6 7 $ pytest -s -q --tb=no test_module.py FFfinalizing <smtplib.SMTP object at 0xdeadbeef0002 > (smtp.gmail.com) ========================= short test summary info ========================== FAILED test_module.py::test_ehlo - assert 0 FAILED test_module.py::test_noop - assert 0 2 failed in 0.12 s
让我们快速创建另一个测试模块,该模块实际上在其模块命名空间中设置服务器 URL:
1 2 3 4 5 6 7 content of test_anothersmtp.py smtpserver = "mail.python.org" def test_showhelo (smtp_connection ): assert 0 , smtp_connection.helo()
Running it:
1 2 3 4 5 6 7 8 9 10 11 12 $ pytest -qq --tb=short test_anothersmtp.py F [100 %] ================================= FAILURES ================================= ______________________________ test_showhelo _______________________________ test_anothersmtp.py:6 : 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 0xdeadbeef0003 > (mail.python.org) ========================= short test summary info ========================== FAILED test_anothersmtp.py::test_showhelo - AssertionError: (250 , b'mail....
瞧! smtp_connection fixture从模块命名空间中获取我们的邮件服务器名称。
通过request.module获取测试模块对象,request是一个对象,这个对象就是整场测试,
request是一个特俗的fixute用于获取有关当前测试的信息和操作
4.8 使用标记将数据传递到fixtures 使用request对象,夹具还可以访问应用于测试功能的标记。这对于将数据从测试传递到夹具中非常有用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import pytest@pytest.fixture def fixt (request ): marker = request.node.get_closest_marker("fixt_data" ) if marker is None : data = None else : data = marker.args[0 ] return data@pytest.mark.fixt_data(42 ) def test_fixt (fixt ): assert fixt == 42
4.9 工厂 fixtures【重点】 “工厂即夹具”模式可以在单次测试中多次需要夹具结果的情况下提供帮助。
夹具不是直接返回数据,而是返回生成数据的函数。然后可以在测试中多次调用该函数。
这里需要了解一下什么是工厂函数,或者说工厂设计模式了,工厂可以根据条件或者参数的不同来创建不同类型的对象
演员可以根据需要拥有参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @pytest.fixture def make_customer_record (): def _make_customer_record (name ): print (f"Making customer record for {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 @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" )
fixture
可以定义成一个工厂方法供测试用例去使用
4.10 参数化 fixtures【重点】 夹具函数可以参数化,在这种情况下,它们将被多次调用,每次执行一组相关测试,即依赖于该夹具的测试。
测试函数通常不需要知道它们的重新运行。夹具参数化有助于为组件编写详尽的功能测试,这些组件本身可以通过多种方式进行配置。
展前面的示例,我们可以标记该固定装置以创建两个 smtp_connection 固定装置实例,这将导致使用该固定装置的所有测试运行两次。
Fixture函数通过特殊的请求对象来访问每个参数:
1 2 3 4 5 6 7 8 9 10 11 import smtplibimport pytest@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 (f"finalizing {smtp_connection} " ) smtp_connection.close()
主要需要了解的是使用 @pytest.fixture 声明参数,这是一个值列表,其中的每个值将被执行,并且可以通过 request.param 访问值。
写一个简单的更容易理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @pytest.fixture(scope="session" , params=[ ['2' , 'two' ],['1' , 'one' ]] ) def get_param (request ): return request.paramdef test_getParam (get_param ): print (f"第{get_param[0 ]} 次运行,打印{get_param[1 ]} " ) ============================= test session starts ============================== platform linux -- Python 3.10 .12 , pytest-8.1 .1 , pluggy-1.4 .0 configfile: pytest.ini plugins: anyio-4.2 .0 collected 2 items testcases/test_fixturestest.py 第1 次运行,打印one .第2 次运行,打印two . ============================== 2 passed in 0.01 s ======================
这里可以看到,这个测试函数,针对get_param方法,运行了两次,
pytest 可以使用字符串进行标记,这个字符串是参数化fixture夹具中每个夹具值的测试 ID 标签,。这些 ID 可以与 -k 一起使用来选择运行的指定的测试用例,并且它们也支持失败重试,
也可以在命令行执行测试时加入 --collect-only
命令可以在运行测试时展示对应的 ID
数字、字符串、布尔值和 None 将在测试 ID 中使用其常用的字符串表示形式。
对于其他对象,pytest 将根据参数名称创建一个字符串。通过使用 ids 关键字参数,可以为某个固定装置值自定义测试 ID 中使用的字符串:
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 54 55 56 57 58 59 60 61 import pytest@pytest.fixture(params=[0 , 1 ], ids=["spam" , "ham" ] ) def a (request ): print (request.param) return request.paramdef test_a (a ): pass ========================= test session starts ============================== platform linux -- Python 3.10 .12 , pytest-8.1 .1 , pluggy-1.4 .0 -- /home/wang/code/venvadmin/bin /python cachedir: .pytest_cache rootdir: /home/ configfile: pytest.ini plugins: anyio-4.2 .0 collecting ... collected 2 items testcases/test_fixturestest.py::test_a[ham] 1 PASSED testcases/test_fixturestest.py::test_a[spam] 0 PASSED ============================== 2 passed in 0.01 s ========def idfn (fixture_value ): if fixture_value == 0 : print ("eggs" ) return "eggs" else : print ("执行了 " ) return None @pytest.fixture(params=[0 , 1 ], ids=idfn ) def b (request ): print (request.param) return request.paramdef test_b (b ): pass ====================== test session starts ============================== platform linux -- Python 3.10 .12 , pytest-8.1 .1 , pluggy-1.4 .0 -- /home/wang/code//venvadmin/bin /python cachedir: .pytest_cache rootdir: /home/ configfile: pytest.ini plugins: anyio-4.2 .0 collecting ... eggs 执行了 collected 2 items testcases/test_fixturestest.py::test_b[1 ] 1 PASSED testcases/test_fixturestest.py::test_b[eggs] 0 PASSED ============================== 2 passed in 0.01 s ===============
也可以在pytest.ini
里面使用-k
接字符串来指定标签运行
1 2 3 4 5 6 [pytest] addopts = -s -v -k "ham"
4.11 使用带标记参数化fixture夹具 pytest.param()
可用于在参数化装置的值集中应用标记,其方式与与 @pytest.mark.parametrize 使用的方式相同。
示例:
1 2 3 4 5 6 7 8 9 10 11 import pytest@pytest.fixture(params=[0 , 1 , pytest.param(2 , marks=pytest.mark.skip )] ) def data_set (request ): return request.paramdef test_data (data_set ): pass
运行此测试将跳过对值为 2 的 data_set 的调用:
1 2 3 4 5 6 7 8 9 10 11 12 $ pytest test_fixture_marks.py -v =========================== test session starts ============================ platform linux -- Python 3. x.y, pytest-8. x.y, pluggy-1. x.y -- $PYTHON_PREFIX/bin /python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 3 items test_fixture_marks.py::test_data[0 ] PASSED [ 33 %] test_fixture_marks.py::test_data[1 ] PASSED [ 66 %] test_fixture_marks.py::test_data[2 ] SKIPPED (unconditional skip) [100 %] ======================= 2 passed, 1 skipped in 0.12 s =======================
4.12 模块化:从一个fixutre方法中使用fixture 模块化:使用固定功能中的固定装置
除了在测试函数中使用固定装置之外,固定装置函数还可以使用其他固定装置本身
这有助于设备的模块化设计,并允许在许多项目中重复使用特定于框架的设备。举个简单的例子,
我们可以扩展前面的示例并实例化一个对象应用程序,将已定义的 smtp_connection 资源粘贴到其中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import pytestclass App : 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
由于smtp_connection的参数化,测试将使用两个不同的App实例和各自的smtp服务器运行两次。应用程序fixture不需要知道smtp_connection参数化,因为pytest将完全分析fixture依赖关系图。
注意,app fixture的作用域是module,并使用了一个模块作用域的smtp_connection fixture。如果smtp_connection缓存在会话作用域中,这个例子仍然可以工作:fixture可以使用“更广”作用域的fixture,但反之则不行:会话作用域的fixture不能以有意义的方式使用模块作用域的fixture。
4.13 按fixture实例自动对测试进行分组 Pytest在测试运行期间最小化活动fixture的数量。如果您有一个参数化的fixture,那么使用它的所有测试将首先使用一个实例执行,然后在创建下一个fixture实例之前调用finalizers。除此之外,这简化了创建和使用全局状态的应用程序的测试。
下面的例子使用了两个参数化的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 23 24 25 26 27 28 29 30 import pytest@pytest.fixture(scope="module" , params=["mod1" , "mod2" ] ) def modarg (request ): param = request.param print (" SETUP modarg" , param) yield param print (" TEARDOWN modarg" , param)@pytest.fixture(scope="function" , params=[1 , 2 ] ) def otherarg (request ): param = request.param print (" SETUP otherarg" , param) yield param print (" TEARDOWN otherarg" , param)def test_0 (otherarg ): print (" RUN test0 with otherarg" , otherarg)def test_1 (modarg ): print (" RUN test1 with modarg" , modarg)def test_2 (otherarg, modarg ): print (f" RUN test2 with otherarg {otherarg} and modarg {modarg} " )
总共执行了8次,让我们在详细模式下运行测试并查看打印输出:详细模式,就是加了-s的模式
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 ======================= test session starts ============================== platform linux -- Python 3.10 .12 , pytest-8.1 .1 , pluggy-1.4 .0 -- /home/wang/code//venvadmin/bin /python cachedir: .pytest_cache rootdir: /home/wang/ configfile: pytest.ini plugins: anyio-4.2 .0 collecting ... collected 8 items testcases/test_fixturestest.py::test_0[1 ] SETUP otherarg 1 RUN test0 with otherarg 1 PASSED TEARDOWN otherarg 1 testcases/test_fixturestest.py::test_0[2 ] SETUP otherarg 2 RUN test0 with otherarg 2 PASSED TEARDOWN otherarg 2 testcases/test_fixturestest.py::test_1[mod1] SETUP modarg mod1 RUN test1 with modarg mod1 PASSED testcases/test_fixturestest.py::test_2[mod1-1 ] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 PASSED TEARDOWN otherarg 1 testcases/test_fixturestest.py::test_2[mod1-2 ] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 PASSED TEARDOWN otherarg 2 testcases/test_fixturestest.py::test_1[mod2] TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 PASSED testcases/test_fixturestest.py::test_2[mod2-1 ] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 PASSED TEARDOWN otherarg 1 testcases/test_fixturestest.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.01 s =============
您可以看到,参数化的模块范围的建模资源导致了测试执行的排序,从而导致了最少可能的“活动”资源。mod1参数化资源的终结器在mod2资源被设置之前被执行。
特别要注意的是,test_0是完全独立的,并且首先完成。然后用mod1执行test_1,然后用mod1执行test_2,然后用mod2执行test_1,最后用mod2执行test_2。
其他参数化资源(具有功能范围)在每次使用它的测试之前设置并在测试之后删除。
4.14 通过 usefixtures 在类和模块中使用fixtures装置 有时测试函数不直接需要访问fixture对象。
例如,测试可能需要使用空目录作为当前工作目录,但不关心具体目录。
下面是如何使用标准的tempfile和pytest fixture来实现它。我们将fixture的创建分离到一个conftest.py文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import osimport tempfileimport pytest@pytest.fixture def cleandir (): with tempfile.TemporaryDirectory() as newpath: old_cwd = os.getcwd() os.chdir(newpath) yield os.chdir(old_cwd)
并通过 usefixtures 标记在测试模块中声明其使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import osimport pytest@pytest.mark.usefixtures("cleandir" ) class TestDirectoryInit : def test_cwd_starts_empty (self ): assert os.listdir(os.getcwd()) == [] with open ("myfile" , "w" , encoding="utf-8" ) as f: f.write("hello" ) def test_cwd_again_starts_empty (self ): assert os.listdir(os.getcwd()) == []
由于使用了usefixtures标记,每个测试方法的执行都需要cleandir fixture,就像您为每个测试方法指定了一个“cleandir”函数参数一样。让我们运行它来验证我们的夹具是否被激活并且测试通过了:
1 2 3 $ pytest -q .. [100 %]2 passed in 0.12 s
您可以像这样指定多个装置:
1 2 @pytest.mark.usefixtures("cleandir" , "anotherfixture" ) def test (): ...
您可以使用 pytestmark
在测试模块级别指定夹具的使用:
1 pytestmark = pytest.mark.usefixtures("cleandir" )
还可以将项目中所有测试所需的装置放入 ini 文件中:
1 2 3 [pytest] usefixtures = cleandir
Warning
请注意,该标记对夹具功能没有影响。例如,这不会按预期工作 :
1 2 3 @pytest.mark.usefixtures("my_other_fixture" ) @pytest.fixture def my_fixture_that_sadly_wont_use_my_other_fixture (): ...
This generates a deprecation warning, and will become an error in Pytest 8.
4.15 覆盖各个级别的同名fixtures装置 在相对较大的测试套件中,您很可能需要使用本地定义的fixture覆盖全局fixture或根fixture,以保持测试代码的可读性和可维护性。
4.15.1 覆盖conftest.py文件内定义的文件夹(包)级别的固定装置 结构如下
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'
4.15.2 覆盖conftest.py文件内定义的模块(.py)级别的固定装置 结构如下:
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 tests/ __init__.py conftest.py import pytest @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'
在上面的示例中,可以为某些测试模块覆盖具有相同名称的夹具。
4.15.3 通过参数化夹具覆盖conftest.py内定义的fixture夹具 目录结构如下:
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'
在上面的示例中,夹具值被测试参数值覆盖。请注意,即使测试不直接使用夹具的值(在函数原型中未提及),也可以通过这种方式覆盖它。
4.15.4 反之亦然,用非参数化夹具覆盖参数化夹具 Given the tests file structure is:
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'
在上面的例子中,一个参数化的夹具被一个非参数化的版本覆盖,而一个非参数化的夹具被一个特定测试模块的参数化的版本覆盖。这同样适用于测试文件夹级别。
4.16 使用其他项目的fixture【不建议】 通常,提供pytest支持的项目将使用入口点,因此只需将这些项目安装到环境中,就可以使用这些fixture。
如果您想使用不使用入口点的项目中的fixture,您可以在顶部conftest.py文件中定义pytest_plugins,以将该模块注册为插件。
假设在mylibrary中有一些fixture。你想在app/tests目录中重用它们。
你所需要做的就是在app/tests/conftest.py中定义pytest_plugins,并指向该模块。
1 pytest_plugins = "mylibrary.fixtures"
这有效地注册了mylibrary。fixture作为一个插件,使其所有fixture和钩子可用于app/tests中的测试。
请注意 有时用户会从其他项目导入fixture以供使用,但不建议这样做:将fixture导入到模块中会将它们注册到pytest中,就像该模块中定义的那样。
这有轻微的后果,比如在pytest——help中出现多次,但不建议这样做,因为这种行为可能会在未来的版本中改变/停止工作。