前言

前面我发了一篇文章是讲pytest框架的基本使用的,这篇文章呢就是讲pytest-fixtures,我也不多说废话了我们直接进入正题吧。

介绍

pytest fixtures的目的是提供一个固定的基线,使测试可以在此基础上可靠地、重复地执行;对比xUnit经典的setup/teardown形式,它在以下方面有了明显的改进:

  • fixture拥有一个明确的名称,通过声明使其能够在函数、类、模块,甚至整个测试会话中被激活使用;
  • fixture以一种模块化的方式实现。因为每一个fixture的名字都能触发一个fixture函数,而这个函数本身又能调用其它的fixture;
  • fixture的管理从简单的单元测试扩展到复杂的功能测试,允许通过配置和组件选项参数化fixture和测试用例,或者跨功能、类、模块,甚至整个测试会话复用fixture;

此外,pytest继续支持经典的xUnit风格的测试。你可以根据自己的喜好,混合使用两种风格,或者逐渐过渡到新的风格。你也可以从已有的unittest.TestCase或者nose项目中执行测试;

1.fixture:作为形参使用

测试用例可以接收fixture的名字作为入参,其实参是对应的fixture函数的返回值。通过@pytest.fixture装饰器可以注册一个fixture;

我们来看一个简单的测试模块,它包含一个fixture和一个使用它的测试用例:

# src/chapter-4/test_smtpsimple.pyimport pytest@pytest.fixture
def smtp_connection():import smtplibreturn smtplib.SMTP("smtp.163.com", 25, timeout=5)def test_ehlo(smtp_connection):response, _ = smtp_connection.ehlo()assert response == 250assert 0  # 为了展示,强制置为失败

这里,test_ehlo有一个形参smtp_connection,和上面定义的fixture函数同名;

执行:

$ pipenv run pytest -q src/chapter-4/test_smtpsimple.py
F                                                                 [100%]
=============================== FAILURES ================================
_______________________________ test_ehlo _______________________________smtp_connection = <smtplib.SMTP object at 0x105992d68>def test_ehlo(smtp_connection):response, _ = smtp_connection.ehlo()assert response == 250
>       assert 0  # 为了展示,强制置为失败
E       assert 0src/chapter-4/test_smtpsimple.py:35: AssertionError
1 failed in 0.17s

执行的过程如下:

  • pytest收集到测试用例test_ehlo,其有一个形参smtp_connection,pytest查找到一个同名的已经注册的fixture;
  • 执行smtp_connection()创建一个smtp_connection实例<smtplib.SMTP object at 0x105992d68>作为test_ehlo的实参;
  • 执行test_ehlo(<smtplib.SMTP object at 0x105992d68>);

如果你不小心拼写出错,或者调用了一个未注册的fixture,你会得到一个fixture <...> not found的错误,并告诉你目前所有可用的fixture,如下:

$ pipenv run pytest -q src/chapter-4/test_smtpsimple.py
E                                                                 [100%]
================================ ERRORS =================================
______________________ ERROR at setup of test_ehlo ______________________
file /Users/yaomeng/Private/Projects/pytest-chinese-doc/src/chapter-4/test_smtpsimple.py, line 32def test_ehlo(smtp_connectio):
E       fixture 'smtp_connectio' not found
>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, smtp_connection, smtp_connection_package, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them./Users/yaomeng/Private/Projects/pytest-chinese-doc/src/chapter-4/test_smtpsimple.py:32
1 error in 0.02s

注意:

你也可以使用如下调用方式:

pytest --fixtures [testpath]

它会帮助你显示所有可用的 fixture;

但是,对于_开头的fixture,需要加上-v选项;

2.fixture:一个典型的依赖注入的实践

fixture允许测试用例可以轻松的接收和处理特定的需要预初始化操作的应用对象,而不用过分关心导入/设置/清理的细节;这是一个典型的依赖注入的实践,其中,fixture扮演者注入者(injector)的角色,而测试用例扮演者消费者(client)的角色;

以上一章的例子来说明:test_ehlo测试用例需要一个smtp_connection的连接对象来做测试,它只关心这个连接是否有效和可达,并不关心它的创建过程。smtp_connection对test_ehlo来说,就是一个需要预初始化操作的应用对象,而这个预处理操作是在fixture中完成的;简而言之,test_ehlo说:“我需要一个SMTP连接对象。”,然后,pytest就给了它一个,就这么简单。

关于依赖注入的解释,可以看看Stackflow上这个问题的高票回答如何向一个5岁的孩子解释依赖注入?:

When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn't want you to have. You might even be looking for something we don't even have or which has expired.

What you should be doing is stating a need, "I need something to drink with lunch," and then we will make sure you have something when you sit down to eat.

更详细的资料可以看看维基百科Dependency injection;

3.conftest.py:共享fixture实例

如果你想在多个测试模块中共享同一个fixture实例,那么你可以把这个fixture移动到conftest.py文件中。在测试模块中你不需要手动的导入它,pytest会自动发现,fixture的查找的顺序是:测试类、测试模块、conftest.py、最后是内置和第三方的插件;

你还可以利用conftest.py文件的这个特性为每个目录实现一个本地化的插件

4. 共享测试数据

如果你想多个测试共享同样的测试数据文件,我们有两个好方法实现这个:

  • 把这些数据加载到fixture中,测试中再使用这些fixture;
  • 把这些数据文件放到tests文件夹中,一些第三方的插件能帮助你管理这方面的测试,例如:pytest-datadirpytest-datafiles

5. 作用域:在跨类的、模块的或整个测试会话的用例中,共享fixture实例

需要使用到网络接入的fixture往往依赖于网络的连通性,并且创建过程一般都非常耗时;

我们来扩展一下上述示例(src/chapter-4/test_smtpsimple.py):在@pytest.fixture装饰器中添加scope='module'参数,使每个测试模块只调用一次smtp_connection(默认每个用例都会调用一次),这样模块中的所有测试用例将会共享同一个fixture实例;其中,scope参数可能的值都有:function(默认值)、class、module、package和session;

首先,我们把smtp_connection()提取到conftest.py文件中:

# src/chapter-4/conftest.pyimport pytest
import smtplib@pytest.fixture(scope='module')
def smtp_connection():return smtplib.SMTP("smtp.163.com", 25, timeout=5)

然后,在相同的目录下,新建一个测试模块test_module.py,将smtp_connection作为形参传入每个测试用例,它们共享同一个smtp_connection()的返回值:

# src/chapter-4/test_module.pydef test_ehlo(smtp_connection):response, _ = smtp_connection.ehlo()assert response == 250smtp_connection.extra_attr = 'test'assert 0  # 为了展示,强制置为失败def test_noop(smtp_connection):response, _ = smtp_connection.noop()assert response == 250assert smtp_connection.extra_attr == 0  # 为了展示,强制置为失败

最后,让我们来执行这个测试模块:

pipenv run pytest -q src/chapter-4/test_module.py
FF                                                                [100%]
=============================== FAILURES ================================
_______________________________ test_ehlo _______________________________smtp_connection = <smtplib.SMTP object at 0x107193c50>def test_ehlo(smtp_connection):response, _ = smtp_connection.ehlo()assert response == 250smtp_connection.extra_attr = 'test'
>       assert 0  # 为了展示,强制置为失败
E       assert 0src/chapter-4/test_module.py:27: AssertionError
_______________________________ test_noop _______________________________smtp_connection = <smtplib.SMTP object at 0x107193c50>def test_noop(smtp_connection):response, _ = smtp_connection.noop()assert response == 250
>       assert smtp_connection.extra_attr == 0
E       AssertionError: assert 'test' == 0
E        +  where 'test' = <smtplib.SMTP object at 0x107193c50>.extra_attrsrc/chapter-4/test_module.py:33: AssertionError
2 failed in 0.72s

可以看到:

  • 两个测试用例使用的smtp_connection实例都是<smtplib.SMTP object at 0x107193c50>,说明smtp_connection只被调用了一次;
  • 在前一个用例test_ehlo中修改smtp_connection实例(上述例子中,为smtp_connection添加extra_attr属性),也会反映到test_noop用例中;

如果你期望拥有一个会话级别作用域的fixture,可以简单的将其声明为:

@pytest.fixture(scope='session')
def smtp_connection():return smtplib.SMTP("smtp.163.com", 25, timeout=5)

注意:

pytest每次只缓存一个fixture实例,当使用参数化的fixture时,pytest可能会在声明的作用域内多次调用这个fixture;

5.1.package作用域(实验性的)

在 pytest 3.7 的版本中,正式引入了package作用域。

package作用域的fixture会作用于包内的每一个测试用例:

首先,我们在src/chapter-4目录下创建如下的组织:

chapter-4/
└── package_expr├── __init__.py├── test_module1.py└── test_module2.py

然后,在src/chapter-4/conftest.py中声明一个package作用域的fixture:

@pytest.fixture(scope='package')
def smtp_connection_package():return smtplib.SMTP("smtp.163.com", 25, timeout=5)

接着,在src/chapter-4/package_expr/test_module1.py中添加如下测试用例:

def test_ehlo_in_module1(smtp_connection_package):response, _ = smtp_connection_package.ehlo()assert response == 250assert 0  # 为了展示,强制置为失败def test_noop_in_module1(smtp_connection_package):response, _ = smtp_connection_package.noop()assert response == 250assert 0  # 为了展示,强制置为失败

同样,在src/chapter-4/package_expr/test_module2.py中添加如下测试用例:

def test_ehlo_in_module2(smtp_connection_package):response, _ = smtp_connection_package.ehlo()assert response == 250assert 0  # 为了展示,强制置为失败

最后,执行src/chapter-4/package_expr下所有的测试用例:

$ pipenv run pytest -q src/chapter-4/package_expr/
FFF                                                               [100%]
=============================== FAILURES ================================
_________________________ test_ehlo_in_module1 __________________________smtp_connection_package = <smtplib.SMTP object at 0x1028fec50>def test_ehlo_in_module1(smtp_connection_package):response, _ = smtp_connection_package.ehlo()assert response == 250
>       assert 0  # 为了展示,强制置为失败
E       assert 0src/chapter-4/package_expr/test_module1.py:26: AssertionError
_________________________ test_noop_in_module1 __________________________smtp_connection_package = <smtplib.SMTP object at 0x1028fec50>def test_noop_in_module1(smtp_connection_package):response, _ = smtp_connection_package.noop()assert response == 250
>       assert 0
E       assert 0src/chapter-4/package_expr/test_module1.py:32: AssertionError
_________________________ test_ehlo_in_module2 __________________________smtp_connection_package = <smtplib.SMTP object at 0x1028fec50>def test_ehlo_in_module2(smtp_connection_package):response, _ = smtp_connection_package.ehlo()assert response == 250
>       assert 0  # 为了展示,强制置为失败
E       assert 0src/chapter-4/package_expr/test_module2.py:26: AssertionError
3 failed in 0.45s

可以看到:

  • 虽然这三个用例在不同的模块中,但是使用相同的fixture实例,即<smtplib.SMTP object at 0x1028fec50>;

注意:

chapter-4/package_expr可以不包含__init__.py文件,因为pytest发现测试用例的规则没有强制这一点;同样,package_expr/的命名也不需要符合test_*或者*_test的规则;

这个功能标记为实验性的,如果在其实际应用中发现严重的bug,那么这个功能很可能被移除;

6.fixture的实例化顺序

多个fixture的实例化顺序,遵循以下原则:

  • 高级别作用域的(例如:session)先于低级别的作用域的(例如:class或者function)实例化;
  • 相同级别作用域的,其实例化顺序遵循它们在测试用例中被声明的顺序(也就是形参的顺序),或者fixture之间的相互调用关系;
  • 使能autouse的fixture,先于其同级别的其它fixture实例化;

我们来看一个具体的例子:

# src/chapter-4/test_order.pyimport pytestorder = []@pytest.fixture(scope="session")
def s1():order.append("s1")@pytest.fixture(scope="module")
def m1():order.append("m1")@pytest.fixture
def f1(f3):order.append("f1")@pytest.fixture
def f3():order.append("f3")@pytest.fixture(autouse=True)
def a1():order.append("a1")@pytest.fixture
def f2():order.append("f2")def test_order(f1, m1, f2, s1):assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]

  • s1拥有最高级的作用域(session),即使在测试用例test_order中最后被声明,它也是第一个被实例化的(参照第一条原则)
  • m1拥有仅次于session级别的作用域(module),所以它是第二个被实例化的(参照第一条原则)
  • f1 f2 f3 a1同属于function级别的作用域:
    • 从test_order(f1, m1, f2, s1)形参的声明顺序中,可以看出,f1比f2先实例化(参照第二条原则)
    • f1的定义中又显式的调用了f3,所以f3比f1先实例化(参照第二条原则)
    • a1的定义中使能了autouse标记,所以它会在同级别的fixture之前实例化,这里也就是在f3 f1 f2之前实例化(参照第三条原则)
  • 所以这个例子fixture实例化的顺序为:s1 m1 a1 f3 f1 f2

注意:

除了autouse的fixture,需要测试用例显示声明(形参),不声明的不会被实例化;

多个相同作用域的autouse fixture,其实例化顺序遵循fixture函数名的排序;

7.fixture的清理操作

我们期望在fixture退出作用域之前,执行某些清理性操作(例如,关闭服务器的连接等);

我们有以下几种形式,实现这个功能:

7.1. 使用yield代替return

将fixture函数中的return关键字替换成yield,则yield之后的代码,就是我们要的清理操作;

我们来声明一个包含清理操作的smtp_connection:

# src/chapter-4/conftest.py@pytest.fixture()
def smtp_connection_yield():smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5)yield smtp_connectionprint("关闭SMTP连接")smtp_connection.close()

再添加一个使用它的测试用例:

# src/chapter-4/test_smtpsimple.pydef test_ehlo_yield(smtp_connection_yield):response, _ = smtp_connection_yield.ehlo()assert response == 250assert 0  # 为了展示,强制置为失败

现在,我们来执行它:

λ pipenv run pytest -q -s --tb=no src/chapter-4/test_smtpsimple.py::test_ehlo_yield
F关闭SMTP连接1 failed in 0.18s

我们可以看到在test_ehlo_yield执行完后,又执行了yield后面的代码;

7.2. 使用with写法

对于支持with写法的对象,我们也可以隐式的执行它的清理操作;

例如,上面的smtp_connection_yield也可以这样写:

@pytest.fixture()
def smtp_connection_yield():with smtplib.SMTP("smtp.163.com", 25, timeout=5) as smtp_connection:yield smtp_connection

7.3. 使用addfinalizer方法

fixture函数能够接收一个request的参数,表示测试请求的上下文;我们可以使用request.addfinalizer方法为fixture添加清理函数;

例如,上面的smtp_connection_yield也可以这样写:

@pytest.fixture()
def smtp_connection_fin(request):smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5)def fin():smtp_connection.close()request.addfinalizer(fin)return smtp_connection

注意:

在yield之前或者addfinalizer注册之前代码发生错误退出的,都不会再执行后续的清理操作

8.fixture可以访问测试请求的上下文

fixture函数可以接收一个request的参数,表示测试用例、类、模块,甚至测试会话的上下文环境;

我们可以扩展上面的smtp_connection_yield,让其根据不同的测试模块使用不同的服务器:

# src/chapter-4/conftest.py@pytest.fixture(scope='module')
def smtp_connection_request(request):server, port = getattr(request.module, 'smtp_server', ("smtp.163.com", 25))with smtplib.SMTP(server, port, timeout=5) as smtp_connection:yield smtp_connectionprint("断开 %s:%d" % (server, port))

在测试模块中指定smtp_server:

# src/chapter-4/test_request.pysmtp_server = ("mail.python.org", 587)def test_163(smtp_connection_request):response, _ = smtp_connection_request.ehlo()assert response == 250

我们来看看效果:

λ pipenv run pytest -q -s src/chapter-4/test_request.py
.断开 mail.python.org:5871 passed in 4.03s

9.fixture返回工厂函数

如果你需要在一个测试用例中,多次使用同一个fixture实例,相对于直接返回数据,更好的方法是返回一个产生数据的工厂函数;

并且,对于工厂函数产生的数据,也可以在fixture中对其管理:

@pytest.fixture
def make_customer_record():# 记录生产的数据created_records = []# 工厂def _make_customer_record(name):record = models.Customer(name=name, orders=[])created_records.append(record)return recordyield _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")

10.fixture的参数化

如果你需要在一系列的测试用例的执行中,每轮执行都使用同一个fixture,但是有不同的依赖场景,那么可以考虑对fixture进行参数化;这种方式适用于对多场景的功能模块进行详尽的测试;

在之前的章节fixture可以访问测试请求的上下文中,我们在测试模块中指定不同smtp_server,得到不同的smtp_connection实例;

现在,我们可以通过指定params关键字参数创建两个fixture实例,每个实例供一轮测试使用,所有的测试用例执行两遍;在fixture的声明函数中,可以使用request.param获取当前使用的入参;

# src/chapter-4/test_request.py@pytest.fixture(scope='module', params=['smtp.163.com', "mail.python.org"])
def smtp_connection_params(request):server = request.paramwith smtplib.SMTP(server, 587, timeout=5) as smtp_connection:yield smtp_connection

在测试用例中使用这个fixture:

# src/chapter-4/test_params.pydef test_parames(smtp_connection_params):response, _ = smtp_connection_params.ehlo()assert response == 250

执行:

$ pipenv run pytest -q -s src/chapter-4/test_params.py
.断开 smtp.163.com:25
.断开 smtp.126.com:252 passed in 0.26s

可以看到:

  • 这个测试用例使用不同的SMTP服务器,执行了两次;

在参数化的fixture中,pytest为每个fixture实例自动指定一个测试ID,例如:上述示例中的test_parames[smtp.163.com]和test_parames[smtp.126.com];

使用-k选项执行一个指定的用例:

$ pipenv run pytest -q -s -k 163 src/chapter-4/test_params.py
.断开 smtp.163.com:251 passed, 1 deselected in 0.16s

使用--collect-only可以显示这些测试ID,而不执行用例:

$ pipenv run pytest -q -s --collect-only src/chapter-4/test_params.py
src/chapter-4/test_params.py::test_parames[smtp.163.com]
src/chapter-4/test_params.py::test_parames[smtp.126.com]no tests ran in 0.01s

同时,也可以使用ids关键字参数,自定义测试ID:

# src/chapter-4/test_ids.py@pytest.fixture(params=[0, 1], ids=['spam', 'ham'])
def a(request):return request.paramdef test_a(a):pass

执行--collect-only:

$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_a
src/chapter-4/test_ids.py::test_a[spam]
src/chapter-4/test_ids.py::test_a[ham]no tests ran in 0.01s

我们看到,测试ID为我们指定的值;

数字、字符串、布尔值和None在测试ID中使用的是它们的字符串表示形式:

# src/chapter-4/test_ids.pydef idfn(fixture_value):if fixture_value == 0:return "eggs"elif fixture_value == 1:return Falseelif fixture_value == 2:return Noneelse:return fixture_value@pytest.fixture(params=[0, 1, 2, 3], ids=idfn)
def b(request):return request.paramdef test_b(b):pass

执行--collect-only:

$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_b
src/chapter-4/test_ids.py::test_b[eggs]
src/chapter-4/test_ids.py::test_b[False]
src/chapter-4/test_ids.py::test_b[2]
src/chapter-4/test_ids.py::test_b[3]no tests ran in 0.01s

可以看到:

  • ids可以接收一个函数,用于生成测试ID;
  • 测试ID指定为None时,使用的是params原先对应的值;

注意:

当测试params中包含元组、字典或者对象时,测试ID使用的是fixture函数名+param的下标:

# src/chapter-4/test_ids.py class C: pass @pytest.fixture(params=[(1, 2), {'d': 1}, C()]) def c(request): return request.param def test_c(c): pass

执行--collect-only:

$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_c src/chapter-4/test_ids.py::test_c[c0] src/chapter-4/test_ids.py::test_c[c1] src/chapter-4/test_ids.py::test_c[c2] no tests ran in 0.01s

可以看到,测试ID为fixture的函数名(c)加上对应param的下标(从0开始);

如果你不想这样,可以使用str()方法或者复写__str__()方法;

11. 在参数化的fixture中标记用例

在fixture的params参数中,可以使用pytest.param标记这一轮的所有用例,其用法和在pytest.mark.parametrize中的用法一样;

# src/chapter-4/test_fixture_marks.pyimport pytest@pytest.fixture(params=[('3+5', 8),pytest.param(('6*9', 42),marks=pytest.mark.xfail,id='failed')])
def data_set(request):return request.paramdef test_data(data_set):assert eval(data_set[0]) == data_set[1]

我们使用pytest.param(('6*9', 42), marks=pytest.mark.xfail, id='failed')的形式指定一个request.param入参,其中marks表示当用例使用这个入参时,为这个用例打上xfail标记;并且,我们还使用id为此时的用例指定了一个测试ID;

$ pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data
============================ test session starts ============================
platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc
collected 2 items                                                           src/chapter-4/test_fixture_marks.py::test_data[data_set0] PASSED      [ 50%]
src/chapter-4/test_fixture_marks.py::test_data[failed] XFAIL          [100%]======================= 1 passed, 1 xfailed in 0.08s ========================

可以看到:

  • 用例结果是XFAIL,而不是FAILED;
  • 测试ID是我们指定的failed,而不是data_set1;

我们也可以使用pytest.mark.parametrize实现相同的效果:

# src/chapter-4/test_fixture_marks.py@pytest.mark.parametrize('test_input, expected',[('3+5', 8),pytest.param('6*9', 42, marks=pytest.mark.xfail, id='failed')])
def test_data2(test_input, expected):assert eval(test_input) == expected

执行:

pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data2
============================ test session starts ============================
platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc
collected 2 items                                                           src/chapter-4/test_fixture_marks.py::test_data2[3+5-8] PASSED         [ 50%]
src/chapter-4/test_fixture_marks.py::test_data2[failed] XFAIL         [100%]======================= 1 passed, 1 xfailed in 0.07s ========================

12. 模块化:fixture使用其它的fixture

你不仅仅可以在测试用例上使用fixture,还可以在fixture的声明函数中使用其它的fixture;这有助于模块化的设计你的fixture,可以在多个项目中重复使用框架级别的fixture;

一个简单的例子,我们可以扩展之前src/chapter-4/test_params.py的例子,实例一个app对象:

# src/chapter-4/test_appsetup.pyimport pytestclass App:def __init__(self, smtp_connection):self.smtp_connection = smtp_connection@pytest.fixture(scope='module')
def app(smtp_connection_params):return App(smtp_connection_params)def test_smtp_connection_exists(app):assert app.smtp_connection

我们创建一个fixture app并调用之前在conftest.py中定义的smtp_connection_params,返回一个App的实例;

执行:

$ pipenv run pytest -v src/chapter-4/test_appsetup.py
============================ test session starts ============================
platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc
collected 2 items                                                           src/chapter-4/test_appsetup.py::test_smtp_connection_exists[smtp.163.com] PASSED [ 50%]
src/chapter-4/test_appsetup.py::test_smtp_connection_exists[smtp.126.com] PASSED [100%]============================= 2 passed in 1.25s =============================

因为app使用了参数化的smtp_connection_params,所以测试用例test_smtp_connection_exists会使用不同的App实例执行两次,并且,app并不需要关心smtp_connection_params的实现细节;

app的作用域是模块级别的,它又调用了smtp_connection_params,也是模块级别的,如果smtp_connection_params是会话级别的作用域,这个例子还是一样可以正常工作的;这是因为低级别的作用域可以调用高级别的作用域,但是高级别的作用域调用低级别的作用域会返回一个ScopeMismatch的异常;

13. 高效的利用fixture实例

在测试期间,pytest只激活最少个数的fixture实例;如果你拥有一个参数化的fixture,所有使用它的用例会在创建的第一个fixture实例并销毁后,才会去使用第二个实例;

下面这个例子,使用了两个参数化的fixture,其中一个是模块级别的作用域,另一个是用例级别的作用域,并且使用print方法打印出它们的setup/teardown流程:

# src/chapter-4/test_minfixture.pyimport pytest@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):param = request.paramprint("  SETUP modarg", param)yield paramprint("  TEARDOWN modarg", param)@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):param = request.paramprint("  SETUP otherarg", param)yield paramprint("  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("  RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))

执行:

$ pipenv run pytest -q -s src/chapter-4/test_minfixture.py SETUP otherarg 1RUN test0 with otherarg 1
.  TEARDOWN otherarg 1SETUP otherarg 2RUN test0 with otherarg 2
.  TEARDOWN otherarg 2SETUP modarg mod1RUN test1 with modarg mod1
.  SETUP otherarg 1RUN test2 with otherarg 1 and modarg mod1
.  TEARDOWN otherarg 1SETUP otherarg 2RUN test2 with otherarg 2 and modarg mod1
.  TEARDOWN otherarg 2TEARDOWN modarg mod1SETUP modarg mod2RUN test1 with modarg mod2
.  SETUP otherarg 1RUN test2 with otherarg 1 and modarg mod2
.  TEARDOWN otherarg 1SETUP otherarg 2RUN test2 with otherarg 2 and modarg mod2
.  TEARDOWN otherarg 2TEARDOWN modarg mod28 passed in 0.02s

可以看出:

  • mod1的TEARDOWN操作完成后,才开始mod2的SETUP操作;
  • 用例test_0独立完成测试;
  • 用例test_1和test_2都使用到了模块级别的modarg,同时test_2也使用到了用例级别的otherarg。它们执行的顺序是,test_1先使用mod1,接着test_2使用mod1和otherarg 1/otherarg 2,然后test_1使用mod2,最后test_2使用mod2和otherarg 1/otherarg 2;也就是说test_1和test_2共用相同的modarg实例,最少化的保留fixture的实例个数;

14. 在类、模块和项目级别上使用fixture实例

有时,我们并不需要在测试用例中直接使用fixture实例;例如,我们需要一个空的目录作为当前用例的工作目录,但是我们并不关心如何创建这个空目录;这里我们可以使用标准的tempfile模块来实现这个功能;

# src/chapter-4/conftest.pyimport pytest
import tempfile
import os@pytest.fixture()
def cleandir():newpath = tempfile.mkdtemp()os.chdir(newpath)

在测试中使用usefixtures标记声明使用它:

# src/chapter-4/test_setenv.pyimport os
import pytest@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit: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标记,测试类TestDirectoryInit中所有的测试用例都可以使用cleandir,这和在每个测试用例中指定cleandir参数是一样的;

执行:

$ pipenv run pytest -q -s src/chapter-4/test_setenv.py
..
2 passed in 0.02s

你可以使用如下方式指定多个fixture:

@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():...

你也可以使用如下方式为测试模块指定fixture:

pytestmark = pytest.mark.usefixtures("cleandir")

注意:参数的名字必须是pytestmark;

你也可以使用如下方式为整个项目指定fixture:

# src/chapter-4/pytest.ini[pytest]
usefixtures = cleandir

注意:

usefixtures标记不适用于fixture声明函数;例如:

@pytest.mark.usefixtures("my_other_fixture") @pytest.fixture def my_fixture_that_sadly_wont_use_my_other_fixture(): ...

这并不会返回任何的错误或告警,具体讨论可以参考#3664

15. 自动使用fixture

有时候,你想在测试用例中自动使用fixture,而不是作为参数使用或者usefixtures标记;设想,我们有一个数据库相关的fixture,包含begin/rollback/commit的体系结构,现在我们希望通过begin/rollback包裹每个测试用例;

下面,通过列表实现一个虚拟的例子:

# src/chapter-4/test_db_transact.pyimport pytestclass DB: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:@pytest.fixture(autouse=True)def transact(self, request, db):db.begin(request.function.__name__)yielddb.rollback()def test_method1(self, db):assert db.intransaction == ["test_method1"]def test_method2(self, db):assert db.intransaction == ["test_method2"]

类级别作用域的transact函数中声明了autouse=True,所以TestClass中的所有用例,可以自动调用transact而不用显式的声明或标记;

执行:

$ pipenv run pytest -q -s src/chapter-4/test_db_transact.py
..
2 passed in 0.01s

autouse=True的fixture在其它级别作用域中的工作流程:

  • autouse fixture遵循scope关键字的定义:如果其含有scope='session',则不管它在哪里定义的,都将只执行一次;scope='class'表示每个测试类执行一次;
  • 如果在测试模块中定义autouse fixture,那么这个测试模块所有的用例自动使用它;
  • 如果在conftest.py中定义autouse fixture,那么它的相同文件夹和子文件夹中的所有测试模块中的用例都将自动使用它;
  • 如果在插件中定义autouse fixture,那么所有安装这个插件的项目中的所有用例都将自动使用它;

上述的示例中,我们期望只有TestClass的用例自动调用fixture transact,这样我们就不希望transact一直处于激活的状态,所以更标准的做法是,将transact声明在conftest.py中,而不是使用autouse=True:

@pytest.fixture
def transact(request, db):db.begin()yielddb.rollback()

并且,在TestClass上声明:

@pytest.mark.usefixtures("transact")
class TestClass:def test_method1(self):...

其它类或者用例也想使用的话,同样需要显式的声明usefixtures;

16. 在不同的层级上覆写fixture

在大型的测试中,你可能需要在本地覆盖项目级别的fixture,以增加可读性和便于维护;

16.1. 在文件夹(conftest.py)层级覆写fixture

假设我们有如下的测试项目:

tests/__init__.pyconftest.py# content of tests/conftest.pyimport pytest@pytest.fixturedef username():return 'username'test_something.py# content of tests/test_something.pydef test_username(username):assert username == 'username'subfolder/__init__.pyconftest.py# content of tests/subfolder/conftest.pyimport pytest@pytest.fixturedef username(username):return 'overridden-' + usernametest_something.py# content of tests/subfolder/test_something.pydef test_username(username):assert username == 'overridden-username'

可以看到:

  • 子文件夹conftest.py中的fixture覆盖了上层文件夹中同名的fixture;
  • 子文件夹conftest.py中的fixture可以轻松的访问上层文件夹中同名的fixture;

16.2. 在模块层级覆写fixture

假设我们有如下的测试项目:

tests/__init__.pyconftest.py# content of tests/conftest.pyimport pytest@pytest.fixturedef username():return 'username'test_something.py# content of tests/test_something.pyimport pytest@pytest.fixturedef username(username):return 'overridden-' + usernamedef test_username(username):assert username == 'overridden-username'test_something_else.py# content of tests/test_something_else.pyimport pytest@pytest.fixturedef username(username):return 'overridden-else-' + usernamedef test_username(username):assert username == 'overridden-else-username'

可以看到:

  • 模块中的fixture覆盖了conftest.py中同名的fixture;
  • 模块中的fixture可以轻松的访问conftest.py中同名的fixture;

16.3. 在用例参数中覆写fixture

假设我们有如下的测试项目:

tests/__init__.pyconftest.py# content of tests/conftest.pyimport pytest@pytest.fixturedef username():return 'username'@pytest.fixturedef other_username(username):return 'other-' + usernametest_something.py# content of tests/test_something.pyimport 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的值被用例的参数所覆盖;
  • 尽管用例test_username_other没有使用username,但是other_username使用到了username,所以也同样受到了影响;

16.4. 参数化的fixture覆写非参数化的fixture,反之亦然

tests/__init__.pyconftest.py# content of tests/conftest.pyimport pytest@pytest.fixture(params=['one', 'two', 'three'])def parametrized_username(request):return request.param@pytest.fixturedef non_parametrized_username(request):return 'username'test_something.py# content of tests/test_something.pyimport pytest@pytest.fixturedef parametrized_username():return 'overridden-username'@pytest.fixture(params=['one', 'two', 'three'])def non_parametrized_username(request):return request.paramdef 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# content of tests/test_something_else.pydef test_username(parametrized_username):assert parametrized_username in ['one', 'two', 'three']def test_username(non_parametrized_username):assert non_parametrized_username == 'username'

可以看出:

  • 参数化的fixture和非参数化的fixture同样可以相互覆盖;
  • 在模块层级上的覆盖不会影响其它模块;

今天就讲到这里啦,各位小伙伴可以动动发财的小手帮我点点赞和关注哟,关注我每天给您不同的惊喜哟。

pytest——fixtures相关推荐

  1. Python单元测试框架之pytest 3 -- fixtures

    From: https://www.cnblogs.com/fnng/p/4769020.html Python单元测试框架之pytest -- fixtures 2015-08-29 13:05 b ...

  2. pytest测试框架_聊聊 Python 的单元测试框架(三):最火的 pytest

    本文首发于 HelloGitHub 公众号,并发表于 Prodesire 博客. 一.介绍 本篇文章是<聊聊 Python 的单元测试框架>的第三篇,前两篇分别介绍了标准库 unittes ...

  3. [翻译]pytest测试框架(二):使用

    此文已由作者吴琪惠授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 调用pytest 调用命令: python -m pytest [...] 上面的命令相当于在命令行直接调用 ...

  4. [翻译]pytest测试框架(一)

    此文已由作者吴琪惠授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 纯官网译文而已... pytest是一个成熟的.全功能的python测试工具. pytest框架编写测试用例 ...

  5. 第十二:Pytest进阶之配置文件

    1.Pytest配置文件能够改变Pytest框架代码的运行规则,比如修改Pytest收集用例的规则,添加命令行参数等等! 2.pytest --help:通过命令pytest --help查看配置文件 ...

  6. 探索pytest的fixture(上)

    在pytest中加入fixture的目的是提供一个固定的基准,使测试能够可靠.重复地执行,pytest的fixture比传统xUnit风格的setup/teardown函数相比,有了巨大的改进: fi ...

  7. pytest使用入门

    pytest是第三方开发的一个python测试模块,可以轻松地编写小型测试,而且可以扩展以支持应用程序和库的复杂功能测试,帮助我们编写更好的程序. 安装pytest 先在命令行中运行pytest的安装 ...

  8. 可能是 Python 中最火的第三方开源测试框架 pytest

    作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...

  9. Pytest测试框架的基本使用和allure测试报告

    一.测试用例的识别与运行 目录识别 通过pytest.ini配置文件配置 如果未指定任何参数,则收集从testpaths(如已配置)或当前目录开始.另外,命令行参数可以在目录.文件名或节点ID的任何组 ...

  10. pytest所有命令行标志都可以通过运行`pytest --help`来获得

    所有命令行标志都可以通过运行pytest --help来获得 (venv) E:\auto_pytest>pytest --help usage: pytest [options] [file_ ...

最新文章

  1. NSCoding 的作用
  2. IOS 面试 --- 动画 block
  3. CSM+3PAR帮助XXX教育技术中心
  4. mysql8.0.19.0_分享MySql8.0.19 安装采坑记录
  5. 【SHARE】WEB前端学习资料
  6. linux硬盘转windows7,记——第一次上手UEFI电脑,将mbr硬盘的Windows7和Linux转为gpt+uefi启动...
  7. c++ 模糊搜索 正则表达式_c++使用正则表达式提取关键字的方法
  8. 斐波那契堆的java实现
  9. iOS13.3越狱插件推荐
  10. 厦门大学计算机学院新院长,厦门大学信息学院对口帮扶座谈会在我院顺利召开...
  11. Cannot create PoolableConnectionFactory (Communications link failure due to unde
  12. 基于java的婚恋交友动态网站
  13. html style属性的用法
  14. EndNote X7如何在论文中嵌入中文格式要求的参考文献
  15. 浏览器开发工具的秘密
  16. net use的用法
  17. php导出excel字体加粗,phpexcel 导出格式,字体调整
  18. 平均值不等式证明(数学归纳法)
  19. week-15(ZJM 与霍格沃兹)
  20. 网文IP风向之变 | 一点财经

热门文章

  1. java毕业设计共享充电宝系统mybatis+源码+调试部署+系统+数据库+lw
  2. 基于Linux下的apache Web 服务
  3. 明源售楼系统技术解析 房源生成(二)
  4. hdu5285 wyh2000 and pupil
  5. 魔众刮刮卡抽奖系统 v2.0.0 支付抽奖,更好用的刮刮卡系统
  6. 2017 苹果强制https
  7. 用Matlab的FDAtool生成IIR滤波器参数
  8. python学习-获取时光网电影TOP100电影信息
  9. 爬虫 -- 简单封装
  10. Python中的turtle.right()方法的用法示例