四、pytest -- fixtures:明确的、模块化的和可扩展的

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

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

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

1. fixture:做为形参使用

测试用例能够接收fixture的名字做为入参,其实参是对应的fixture函数的返回值。经过@pytest.fixture装饰器能够注册一个fixturegithub

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

# src/chapter-4/test_smtpsimple.py

import pytest


@pytest.fixture
def smtp_connection():
    import smtplib

    return smtplib.SMTP("smtp.163.com", 25, timeout=5)


def test_ehlo(smtp_connection):
    response, _ = smtp_connection.ehlo()
    assert response == 250
    assert 0  # 为了展现,强制置为失败

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

执行:bash

$ 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 0

src/chapter-4/test_smtpsimple.py:35: AssertionError
1 failed in 0.17s

执行的过程以下:服务器

  • pytest收集到测试用例test_ehlo,其有一个形参smtp_connectionpytest查找到一个同名的已经注册的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 32
  def 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

注意:session

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

pytest --fixtures [testpath]

它会帮助你显示全部可用的 fixture;

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

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

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

以上一章的例子来讲明:test_ehlo测试用例须要一个smtp_connection的链接对象来作测试,它只关心这个链接是否有效和可达,并不关心它的建立过程。smtp_connectiontest_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(默认值)、classmodulepackagesession

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

# src/chapter-4/conftest.py


import 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.py


def test_ehlo(smtp_connection):
    response, _ = smtp_connection.ehlo()
    assert response == 250
    smtp_connection.extra_attr = 'test'
    assert 0  # 为了展现,强制置为失败


def test_noop(smtp_connection):
    response, _ = smtp_connection.noop()
    assert response == 250
    assert 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 == 250
        smtp_connection.extra_attr = 'test'
>       assert 0  # 为了展现,强制置为失败
E       assert 0

src/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_attr

src/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 == 250
    assert 0  # 为了展现,强制置为失败


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

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

def test_ehlo_in_module2(smtp_connection_package):
    response, _ = smtp_connection_package.ehlo()
    assert response == 250
    assert 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 0

src/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 0

src/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 0

src/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之间的相互调用关系;
  • 使能autousefixture,先于其同级别的其它fixture实例化;

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

# src/chapter-4/test_order.py

import pytest

order = []


@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)形参的声明顺序中,能够看出,f1f2先实例化(参照第二条原则)
    • f1的定义中又显式的调用了f3,因此f3f1先实例化(参照第二条原则)
    • a1的定义中使能了autouse标记,因此它会在同级别的fixture以前实例化,这里也就是在f3 f1 f2以前实例化(参照第三条原则)
  • 因此这个例子fixture实例化的顺序为:s1 m1 a1 f3 f1 f2

注意:

  • 除了autousefixture,须要测试用例显示声明(形参),不声明的不会被实例化;

  • 多个相同做用域的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_connection
    print("关闭SMTP链接")
    smtp_connection.close()

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

# src/chapter-4/test_smtpsimple.py

def test_ehlo_yield(smtp_connection_yield):
    response, _ = smtp_connection_yield.ehlo()
    assert response == 250
    assert 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_connection
        print("断开 %s:%d" % (server, port))

在测试模块中指定smtp_server

# src/chapter-4/test_request.py

smtp_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:587

1 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 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")

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.param
    with smtplib.SMTP(server, 587, timeout=5) as smtp_connection:
        yield smtp_connection

在测试用例中使用这个fixture

# src/chapter-4/test_params.py

def 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:25

2 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:25

1 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.param


def 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.py

def idfn(fixture_value):
    if fixture_value == 0:
        return "eggs"
    elif fixture_value == 1:
        return False
    elif fixture_value == 2:
        return None
    else:
        return fixture_value


@pytest.fixture(params=[0, 1, 2, 3], ids=idfn)
def b(request):
    return request.param


def 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

能够看到,测试IDfixture的函数名(c)加上对应param的下标(从0开始);

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

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

fixtureparams参数中,可使用pytest.param标记这一轮的全部用例,其用法和在pytest.mark.parametrize中的用法同样;

# src/chapter-4/test_fixture_marks.py

import pytest


@pytest.fixture(params=[('3+5', 8),
                        pytest.param(('6*9', 42),
                                     marks=pytest.mark.xfail,
                                     id='failed')])
def data_set(request):
    return request.param


def 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.py

import pytest


class 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.py

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("  RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))

执行:

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

8 passed in 0.02s

能够看出:

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

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

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

# src/chapter-4/conftest.py

import pytest
import tempfile
import os


@pytest.fixture()
def cleandir():
    newpath = tempfile.mkdtemp()
    os.chdir(newpath)

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

# src/chapter-4/test_setenv.py

import 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.py

import pytest


class 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__)
        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,因此TestClass中的全部用例,能够自动调用transact而不用显式的声明或标记;

执行:

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

autouse=Truefixture在其它级别做用域中的工做流程:

  • 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()
    yield
    db.rollback()

而且,在TestClass上声明:

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

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

16. 在不一样的层级上覆写fixture

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

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

假设咱们有以下的测试项目:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

    test_something.py
        # content of tests/test_something.py
        def test_username(username):
            assert username == 'username'

    subfolder/
        __init__.py

        conftest.py
            # content of tests/subfolder/conftest.py
            import pytest

            @pytest.fixture
            def username(username):
                return 'overridden-' + username

        test_something.py
            # content of tests/subfolder/test_something.py
            def test_username(username):
                assert username == 'overridden-username'

能够看到:

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

16.2. 在模块层级覆写fixture

假设咱们有以下的测试项目:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

    test_something.py
        # content of tests/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
        # content of tests/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覆盖了conftest.py中同名的fixture
  • 模块中的fixture能够轻松的访问conftest.py中同名的fixture

16.3. 在用例参数中覆写fixture

假设咱们有以下的测试项目:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

        @pytest.fixture
        def other_username(username):
            return 'other-' + username

    test_something.py
        # content of tests/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的值被用例的参数所覆盖;
  • 尽管用例test_username_other没有使用username,可是other_username使用到了username,因此也一样受到了影响;

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

tests/
    __init__.py

    conftest.py
        # content of tests/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
        # content of tests/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
        # content of tests/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'

能够看出:

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

GitHub仓库地址:https://github.com/luizyao/pytest-chinese-doc

相关文章
相关标签/搜索