pytest探究(2):fixtures

pytest 的 fixtures 基于 xUnit 的 setup/teardown 风格,可对测试进行前置处理和后置处理,以下图片是对 xUnit 风格的总结,详细可以参考上面链接:

image

pytest 的 fixtures 特点如下:

  • fixtures 有显式名字:可从测试函数,模块,类或者整个项目中直接调用 fixture
  • fixtures 采用模块化:每个 fixture 名字都会调用一个 fixture 函数,该函数也可以调用其他 fixtures
  • fixtures 管理从简单的单元扩展到复杂的功能测试,允许根据配置和组件选项对 fixture 和测试进行参数化,或者跨功能、类、模块或整个测试会话范围重用 fixture 。

pytest 支持 classic xunit-style setup 。可以混合这两种样式,从经典样式递增到新样式。也可以使用现有的 unittest.TestCase style 样式或基于 nose 的项目开始。

可以使用 @pytest.fixture 装饰器定义 Fixtures ,比如 官方链接 1。Pytest 也提供许多内置的 fixtures,以下是简述,后面会进行详解 :

capfd

捕获为文本,输出到文件描述符 “1” 和 “2” 。

capfdbinary

Capture, as bytes, output to file descriptors 1 and 2 .

caplog

Control logging and access log entries.

capsys

Capture, as text, output to sys.stdout and sys.stderr .

capsysbinary

Capture, as bytes, output to sys.stdout and sys.stderr .

cache 1

Store and retrieve values across pytest runs.

doctest_namespace

Provide a dict injected into the docstests namespace.

monkeypatch

Temporarily modify classes, functions, dictionaries, os.environ , and other objects.

pytestconfig

Access to configuration values, pluginmanager and plugin hooks.

record_property

Add extra properties to the test.

record_testsuite_property

Add extra properties to the test suite.

recwarn

Record warnings emitted by test functions.

request

Provide information on the executing test function.

testdir

Provide a temporary test directory to aid in running, and testing, pytest plugins.

tmp_path 1

Provide a pathlib.Path object to a temporary directory which is unique to each test function.

tmp_path_factory 1

Make session-scoped temporary directories and return pathlib.Path objects.

tmpdir

Provide a py.path.local object to a temporary directory which is unique to each test function; replaced by tmp_path 1.

tmpdir_factory

Make session-scoped temporary directories and return py.path.local objects; replaced by tmp_path_factory 1.

Fixtures 作为函数参数

测试函数将 fixtures 命名为输入参数,从而来接收它们。 对于每个参数名,与该参数名同名的 fixtures 函数会提供 fixture 对象。 可以用 @pytest.fixture 来注册 fixture 函数。 比如下面例子:

import pytest

@pytest.fixture
def smtp_connection():
    import smtplib

    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert 0  # for demo purposes

test_ehlo 需要 smtp_connection fixture 的值。 pytest 会发现用 @pytest.fixture 标记的 smtp_connection fixture 函数,运行测试结果如下:

$ pytest test_smtpsimple.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item

test_smtpsimple.py F                                                 [100%]

================================= FAILURES =================================
________________________________ test_ehlo _________________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

    def test_ehlo(smtp_connection):
        response, msg = smtp_connection.ehlo()
        assert response == 250
>       assert 0  # for demo purposes
E       assert 0

test_smtpsimple.py:14: AssertionError
========================= short test summary info ==========================
FAILED test_smtpsimple.py::test_ehlo - assert 0

从错误信息可以看出,测试函数需要 smtp_connection 参数, fixture 函数创建了 smtplib.SMTP() 实例,因为代码中是 assert 0 ,所以运行会失败,整个调用流程如下:

  1. pytest发现测试函数 test_ehlo ,由于测试函数需要 smtp_connection 参数,存在与这个参数同名的 fixture 函数。
  2. 调用 smtp_connection() 函数创建实例
  3. 最终调用 test_ehlo(<smtp_connection instance>)函数

如果拼写了错误的函数参数或者这个参数不可用, pytest 会报错,并列出可用的参数。也可以在命令行用 --fixtures 列出所有可用的 fixtures (包括系统内置的 fixture 和用户自定义的 fixture ),考虑以下代码及执行结果:


import pytest

@pytest.fixture()
def get_10():
    return 10

def test_fixture(get_10):
    print(get_10)

使用命令 pytest --fixtures 后,输出结果只截取自己的 fixture ,系统内置的 fixture 没有截取:

[
image

image1618×242 6.26 KB](https://ceshiren.com/uploads/default/original/2X/2/22938485954685e0fe5bd382112ecb31c6f52a42.png)

加上 -v 参数可以展示详细信息,包括 fixture 位置,比如执行 pytest --fixtures -v

[
image

image1606×284 7.94 KB](https://ceshiren.com/uploads/default/original/2X/2/29c638da473c15c8d33463cf797ce34e25180a94.png)

Fixtrues: 依赖注入

fixtures 允许测试函数接收初始化后的对象,而不必关心导入/设置/清理的详细信息。这就是依赖注入模式,了解 spring 的同学应该不陌生, spring 用它解决了组件的依赖项和组件之间的过度耦合问题,在 pytest 也是同理。有关依赖注入的详细内容,可查看 wiki :

https://en.wikipedia.org/wiki/Dependency_injection 1

conftest.py:共享 fixture 函数

有时想在多个测试文件中使用 fixture 函数,就要把 fixture 函数移动到 conftest.py 文件中。 pytest 会自动发现 conftest.py ,你不用手动导入, 发现规则有前有后,依次是类,测试模块,然后是 conftest.py 文件,内置或者第三方插件。

conftest.py 还可以实现 local per-directory plugins

考虑以下目录结构及代码内容:

image
# a/conftest.py文件内容
import pytest
@pytest.fixture()
def tmp():
    return 20

# a/a1/conftest.py文件内容
import pytest
@pytest.fixture()
def tmp():
    return 30

# test_a1.py文件内容

def test_print(tmp):
    print(tmp)

# test_a2.py文件内容

def test_print(tmp):
    print(tmp)

可以看出, pytest_a1.py 输出结果是 30 , pytest_a2.py 输出结果是 20 ,子目录的 conftest.py 内容会覆盖父目录的 conftest.py :

[
image

image2022×341 14.5 KB](https://ceshiren.com/uploads/default/original/2X/f/f4100435115e6911f224f70131932b4e52a952aa.png)[

image

image1882×352 14.4 KB](https://ceshiren.com/uploads/default/original/2X/0/0c38a41870e70d82aaa86642edbd4da1a67b59d8.png)

共享测试数据

如果让测试数据多文件可用,最好把数据放到 fixture 中,这个方法充分利用了 pytest 的自动缓存机制。另一个好方法是把数据移动到 tests 目录,社区提供一些插件帮助管理,比如 pytest-datadirpytest-datafiles

范围:在类,模块或者会话中共享 fixture

@pytest.fixture 参数中添加 scope="module",使其装饰的函数,仅在每个测试模块中实例化一次(默认在每个测试函数前实例化一次),你可以自行打印 fixture 对象,对比区别。比如下面的 smtplib.SMTP ,会创建一个实例,由于它非常依赖网络,所以设置为模块级别可以避免多次创建。 scope 还有更多的值: function , class , module , package 或者 session 。

接下来的例子把 fixture 放到了 conftest.py 文件,所以目录中的多个测试模块都能访问这个函数:

# content of conftest.py
import pytest
import smtplib

@pytest.fixture(scope="module")
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

你可以在 conftest.py 的同目录,或者子目录的测试文件中调用 fixture ,比如下面代码:

# content of test_module.py

def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg
    assert 0  # for demo purposes

def test_noop(smtp_connection):
    response, msg = smtp_connection.noop()
    assert response == 250
    assert 0  # for demo purposes

下面是执行结果,我们故意让其出错( assert 0 ),以便 pytest 可以打印详细信息:

$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 2 items

test_module.py FF                                                    [100%]

================================= FAILURES =================================
________________________________ test_ehlo _________________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

    def test_ehlo(smtp_connection):
        response, msg = smtp_connection.ehlo()
        assert response == 250
        assert b"smtp.gmail.com" in msg
>       assert 0  # for demo purposes
E       assert 0

test_module.py:7: AssertionError
________________________________ test_noop _________________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

    def test_noop(smtp_connection):
        response, msg = smtp_connection.noop()
        assert response == 250
>       assert 0  # for demo purposes
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.12s =============================

assert 0 让 pytest 展示接收到的 smtp_connection 对象,这两个测试函数使用相同的 smtp_connection 对象,即 smtp_connection 只会在整个模块初始化一次。你也可以把 scope 改为 session 级别:

pytest.fixture(scope="session")
def smtp_connection():
    # the returned fixture value will be shared for
    # all tests needing it
    ...

fixture的作用范围

session>module>class>function

  • function:每一个函数或方法都会调用

  • class:每一个类调用一次,一个类中可以有多个方法

  • module:每一个 *.py 文件调用一次,该文件内又有多个 function 和 class

  • session:是多个文件调用一次,可以跨 *.py 文件调用,每个 *.py 文件就是 module

注意:
Pytest 同一时间只会缓存一个 fixture ,当使用参数化的 fixture 时, pytest 会在范围内多次调用 fixture 。 pytest3.7 有包范围,由于是测试版,不建议使用。

动态范围
有时,你不想改写代码,做到动态的改变 fixture 范围。可以定义函数,将之传递给 fixture 的 scope 参数,在 fixture 定义的时候会自动执行这个函数。这个函数必须有两个参数 - fixture_name 字符串 和 config 配置对象。

比如下面生成 docker 容器函数 ,你可以使用命令行参数控制范围:

# conftest.py 内容
from time import sleep

import pytest

def determine_scope(fixture_name, config):
    # 如果发现参数是 --keep-containers
    if config.getoption("--scope_session", None):
        return "session"
    # 返回函数级别
    return "function"

@pytest.fixture(scope=determine_scope)
def create_value():
    sleep(5)

def pytest_addoption(parser):
    parser.addoption("--scope_session", action="store", default=None,
                     help="None")

# test_a1内容

def test_a1(create_value):
    print('a1')

def test_a2(create_value):
    print('a2')

def test_a3(create_value):
    print('a3')

参考以下两种运行结果,加入参数的话, fixture 的范围会被更改成 session 级别, 3 个 test 只会初始化一次 fixture 函数,运行时间为 5s 左右:

[
image

image1877×484 18 KB](https://ceshiren.com/uploads/default/original/2X/b/bf497d8af7140cdbc4a4f84c03323e1048082283.png)

如果没有参数, fixture 的范围会被更改成 function 级别, 3 个 test 会初始化三次 fixture 函数,运行时间为 15s 左右:

(文章来源有霍格沃兹测试学院)

更多技术文章点击获取http://qrcode.testing-studio.com/f?from=jianshu&url=https://ceshiren.com/t/topic/3822

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容