4、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.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函数同名;

执行:

$ 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实例作为test_ehlo的实参;
  • 执行test_ehlo()

如果你不小心拼写出错,或者调用了一个未注册的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

注意:

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

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实例都是,说明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, _ 

你可能感兴趣的:(pytest,中文文档,pytest,python,阅读)