python-pytest内置fixture

上一篇文章已经介绍了pytest的fixture强大之处,但都是我们自己定义的fixture。下面来一起看下pytest内置的fixture。

1. tmpdir和tmpdir_factory

tmpdir 和 tmpdir_factory 负责在测试开始之前创建临时目录和文件,并且在结束的时候删除,也就是创建临时目录文件供测试使用,用完即销毁,不会对测试所在系统产生影响。

tmpdir

tmpdir 的作用范围是函数级别,建议在单个测试的时候使用 tmpdir 。如果每个测试函数都要重新创建目录或文件,也建议使用 tmpdir

def test_tmpdir(tmpdir):
    # tmpdir.join():创建临时文件
    a_file = tmpdir.join('something.txt')
	# tmpdir.mkdir():创建临时目录
    a_sub_dir = tmpdir.mkdir('anything')

    another_file = a_sub_dir.join('something_else.txt')

    a_file.write('contents may settle during shipping')

    another_file.write('something different')

    assert a_file.read() == 'contents may settle during shipping'
    assert another_file.read() == 'something different'

测试结果(在需要的时候使用 -q 参数简化结果输出)

$ pytest -q test_tmpdir.py::test_tmpdir
.                                                                                                                                                                          [100%]
1 passed in 0.02s

测试结果很满意。tmpdir 的返回值是 py.path.local 类型的一个对象(关于 py.path.local 的解释 ),但是需要注意 tmpdir 的作用范围是函数级别,所以只能针对于测试函数,如果需要更高级别的作用范围,就需要使用到 tmpdir_factory

tmpdir_factory

tmpdir_factory 和 tmpdir 用法很像,但是他们还是有不同的接口。它会作用于类、模块以及会话级别的测试。在会话级别的 fixture 中,创建的资源可以持续到测试结束。
下面通过代码来展示两者之间不同的用法

def test_tmpdir_factory(tmpdir_factory):
    # 这里返回的还是一个 py.path.local 对象,tmpdir_factory.mktemp():创建临时目录
    a_dir = tmpdir_factory.mktemp('mydir')

    a_file = a_dir.join('something.txt')
    a_sub_dir = a_dir.mkdir('anything')
    another_file = a_sub_dir.join('something_else.txt')

    a_file.write('contents may settle during shipping')
    another_file.write('something different')

    assert a_file.read() == 'contents may settle during shipping'
    assert another_file.read() == 'something different'

下面是运行结果

$ pytest -q test_tmpdir.py::test_tmpdir_factory
.                                                                                                                                                                          [100%]
1 passed in 0.03s

因为 tmpdir_factory 的作用范围是会话级别,如果我们需要模块或者类级别的作用范围的目录,这时就需要我们自己在定义一个 fixture 了。

import json
import pytest


@pytest.fixture(scope='module')
def some_json(tmpdir_factory):
    json_data = {
        'Nancy': {'City': 'Hangzhou'},
        'Petter': {'City': 'Beijing'},
        'Jim': {'City': 'Shanghai'}
    }

    file = tmpdir_factory.mktemp('data').join('friends.json')
    print('file:{}'.format(str(file)))

    with file.open('w') as f:
        json.dump(json_data, f)
    return file

这样我们创建的这个 fixture 就会创建一个临时目录 data,然后将需要测试的内容写入 friends.json  文件,因为我们在定义的时候 scope="module" ,所以该 json 文件只会在每个模块运行的时候创建一次。

tmpdir_factory 还有很多其他方法在这里没有展示,有兴趣的同学可以去翻一番代码。

2. pytestconfig

内置的 pytestconfig 可以通过命令行参数、选项、配置文件、插件、运行目录等等方式来控制pytest的运行。
直接来看代码

# conftest.py
def pytest_addoption(parser):
    # parser.addoption():为pytest添加命令行参数
    parser.addoption("--testopt", action="store_true",
                     help="this is test msg")
    parser.addoption("--free", action="store", default="beer",
                     help="free: beer or people")

需要注意的是, conftest.py 需要在顶层目录下,不能处于测试子目录下
接下来我们测试上述代码

import pytest


def test_option(pytestconfig):
    print('"testopt" set to:', pytestconfig.getoption('testopt'))
    print('"free" set to:', pytestconfig.getoption('free'))

运行测试

$ pytest -sq test_config.py::test_option
"testopt" set to: False
"free" set to: beer
.
1 passed in 0.01s

$ pytest -sq --testopt test_config.py::test_option
"testopt" set to: True
"free" set to: beer
.
1 passed in 0.01s

$ pytest -sq --testopt --free people test_config.py::test_option
"testopt" set to: True
"free" set to: people
.
1 passed in 0.01s

pytestconfig 本身就是一个fixture,所以我们也可以用在其他fixture上。

@pytest.fixture()
def testopt(pytestconfig):
    return pytestconfig.option.testopt


@pytest.fixture()
def free(pytestconfig):
    return pytestconfig.option.free


def test_fixtures_for_options(testopt, free):
    print('"testopt" set to:', testopt)
    print('"free" set to:', free)
$ pytest -sq --testopt --free people test_config.py::test_fixtures_for_options
"testopt" set to: True
"free" set to: people
.
1 passed in 0.02s

3. cache

我们在测试的时候,有时候需要每个测试之间是相互独立的,保证测试结果不依赖于测试顺序,用不同的测试顺序也可以得到相同的测试结果。
但是,有的时候又希望时候上一次会话的信息,这时候需要使用到 cache 。

–ff or --lf

cache  的作用是存储一段测试会话的信息,在下一段测试会话中使用。使用命令行参数 --lf | --last-failed 或者 --ff | --failed-first 来查看缓存的数据。
测试一段简单的代码

# test_pass_fail.py
def test_this_passes():
    assert 1 == 1


def test_this_fails():
    assert 1 == 2

我们先运行一次测试,使其拥有缓存

$ pytest -q test_pass_fail.py
.F                                                                                                                                                                         [100%]
============================================== FAILURES ====================================================================================
________________________________________________________________________________ test_this_fails _________________________________________________________________________________

    def test_this_fails():
>       assert 1 == 2
E       assert 1 == 2

test_pass_fail.py:6: AssertionError
========================================== short test summary info =============================================================================
FAILED test_pass_fail.py::test_this_fails - assert 1 == 2
1 failed, 1 passed in 0.32s

接下来使用 --ff 参数或 --failed-first ,观察用例的运行顺序

$ pytest -q --ff test_pass_fail.py
F.                                                                                                                                                                         [100%]
============================================ FAILURES ====================================================================================
________________________________________________________________________________ test_this_fails _________________________________________________________________________________

    def test_this_fails():
>       assert 1 == 2
E       assert 1 == 2

test_pass_fail.py:6: AssertionError
==================================== short test summary info =============================================================================
FAILED test_pass_fail.py::test_this_fails - assert 1 == 2
1 failed, 1 passed in 0.18s

可以看到,上次运行失败的用例这次首先运行了,其次才运行上次pass的用例。再看一下 --lf 参数的作用

$ pytest -q --lf test_pass_fail.py
F                                                                                                                                                                          [100%]
==================================================================================== FAILURES ====================================================================================
________________________________________________________________________________ test_this_fails _________________________________________________________________________________

    def test_this_fails():
>       assert 1 == 2
E       assert 1 == 2

test_pass_fail.py:6: AssertionError
============================================================================ short test summary info =============================================================================
FAILED test_pass_fail.py::test_this_fails - assert 1 == 2
1 failed, 1 deselected in 0.28s

--lf 参数可以使测试只运行上次失败的用例。

查看缓存

既然有缓存,一定是存在于某个文件里面的数据,我们可以直接查看这些缓存数据。
--cache-show 是查看缓存的命令行参数
缓存文件都存在于 .pytest_cache 目录下。

4. capsys

capsys 有两个功能

  • 允许使用代码读取 stdout 和 stderr
  • 可以临时禁止抓取日志的输出

我们假设某个函数要把信息输出到 stdout

# test_capsys.py
def print_info(info):
    print('This is ours masseage: ' % info)

函数的定义限制了我们无法使用返回值去测试,只能使用 stdout ,这个时候 capsys 就派上用场了

# test_capsys.py
...
def test_print_info(capsys):
    msg = 'life is short,you need python'
    print_info(msg)
    out, err = capsys.readouterr()
    assert out == 'This is ours masseage: ' + msg + '\n'
    assert err == ''
    
    print_info('test1...')
    print_info('test2...')
    out, err = capsys.readouterr()
    assert out == 'This is ours masseage: test1\n' + 'This is ours masseage: test2...\n'
    assert err == ''

测试结果

$ pytest -q test_capsys.py::test_print_info
.                                                                                                                                                                          [100%]
1 passed in 0.02s

读取到的 stdout 和 stderr 信息是从 capsys.readouterr() 中获取到的。返回值从测试函数运行后捕捉,或者从上次调用中获取。

再来一个 stderr 的例子

def ping(output):
    print('loading...', file=output)


def test_stderr(capsys):
    ping(sys.stderr)
    out, err = capsys.readouterr()
    assert out == ''
    assert err == 'loading...\n'

capsys.disabled() 可以临时让输出绕过默认的输出捕捉机制

# test_capsys_disabled.py
def test_capsys_disabled(capsys):
    with capsys.disabled():
        print('\nalways print this')
    print('normal print, usually captured')

从测试结果中就可以看出, ‘always print this’ 这句话一直会输出,而 ‘normal print, usually captured’ 这句话只有在 -s 参数下才会显示。

$ pytest -q test_capsys.py::test_capsys_disabled

always print this
.                                                                                                                                                                          [100%]
1 passed in 0.03s
$ pytest -qs test_capsys.py::test_capsys_disabled

always print this
normal print, usually captured
.
1 passed in 0.02s

5. monkeypatch

monkey patch 可以在运行期间对类或模块进行动态修改。在测试中, monkey patch 常用于被测试代码的部分运行环境,或者将输入依赖或输出依赖替换成更容易测试的对象或函数。在测试期间的一切修改都会在测试结束后复原。
先来看下 monkeypatch 的API

  • setattr(target,name,value=<notset>,raising=True)  设置一个属性
  • delattr(target,name=<notset>,raising=True)  删除一个属性
  • setitem(dic,name,value)  设置字典中的一条记录
  • delitem(dic,name,raising=True)  删除字典中的一条记录
  • setnev(name,value,prepend=True)  设置一个环境变量
  • delenv(name,raising)  删除一个环境变量
  • syspath_prepend(path)  将路径 path 加入 sys.path 并放在最前面, sys.path 是 Python 导入的系统路径列表
  • chdir(path)  改变当前的工作目录

注: raising 参数用于指示 pytest 是否在记录不存在时抛出异常。 setenv() 函数里的 prepend 参数可以是一个字符,如果是这样设置,环境变量的值就是 value + prepend + <old value>

下面用一段简单的代码来讲述 monkeypatch 在工作中的作用

# cheese.py
import os
import json


def read_cheese_preferences():
    # os.path.enpanduser()会将用户设置的环境变量 HOME ,替换掉参数中的 ~ 。
    full_path = os.path.expanduser('~/.cheese.json')
    with open(full_path, 'r') as f:
        prefs = json.load(f)
    return prefs


def write_cheese_preferences(prefs):
    full_path = os.path.expanduser('~/.cheese.json')
    with open(full_path, 'w') as f:
        json.dump(prefs, f, indent=4)


def write_default_cheese_preferences():
    write_cheese_preferences(_default_prefs)


_default_prefs = {
    'slicing': ['manchego', 'sharp cheddar'],
    'spreadable': ['Saint Andre', 'camembert',
                   'bucheron', 'goat', 'humbolt fog', 'cambozola'],
    'salads': ['crumbled feta']
}

上面的代码假设 write_cheese_preferences() 生成了配置文件。但是既没有参数也没有返回值,我们该如何进行测试?如果是直接运行,用户在之前的预置配置文件 ~/.cheese.json 就会被覆盖,这显然不符合我们的测试要求。所以我们需要创建一个临时的 HOME 目录,并在这个临时目录里面测试。

# test_cheese.py
def test_def_prefs_change_home(tmpdir, monkeypatch):
    # 这里就用到了 setenv() 设置环境变量。先利用 tmpdir 创建了一个临时目录,然后将这个临时
    # 目录指向了 HOME 变量
    monkeypatch.setenv('HOME', tmpdir.mkdir('home'))
    cheese.write_default_cheese_preferences()
    expected = cheese._default_prefs
    actual = cheese.read_cheese_preferences()
    assert expected == actual
$ pytest -qs test_cheese.py::test_def_prefs_change_home
.
1 passed in 0.01s

这样看起来确实可以了,但是这只针对 MacOS / Linux 系统有效,显然不适用于 Windows 用户,所以我们接下来看看另一种实现方式。

# test_cheese.py
...
def test_def_prefs_change_expanduser(tmpdir, monkeypatch):
    fake_home_dir = tmpdir.mkdir('home')
    # 这里将 cheese 模块中的 os.path.expanduser() 函数替换成了下面的匿名函数
    # 将 ~ 替换成了我们自己定义的 home 目录
    monkeypatch.setattr(cheese.os.path, 'expanduser',
                        (lambda x: x.replace('~', str(fake_home_dir))))
    cheese.write_default_cheese_preferences()
    expected = cheese._default_prefs
    actual = cheese.read_cheese_preferences()
    assert expected == actual

从结果看来也是没什么问题的,Windows用户运行起来应该也是OK的。

$ pytest -qs test_cheese.py::test_def_prefs_change_expanduser
.
1 passed in 0.00s

上面已经使用了 setenv() 以及 setattr() ,下面来看一看 setitem()  函数

# test_cheese.py
def test_def_prefs_change_defaults(tmpdir, monkeypatch):
    fake_home_dir = tmpdir.mkdir('home')
    # setattr 用法上面已经说明过
    monkeypatch.setattr(cheese.os.path, 'expanduser',
                        (lambda x: x.replace('~', str(fake_home_dir))))
    cheese.write_default_cheese_preferences()
    # 在修改 item 之前先进行深拷贝,作为后面断言的对象
    defaults_before = copy.deepcopy(cheese._default_prefs)

    # 利用 setitem 属性来改变默认字典的值
    monkeypatch.setitem(cheese._default_prefs, 'slicing', ['provolone'])
    monkeypatch.setitem(cheese._default_prefs, 'spreadable', ['brie'])
    monkeypatch.setitem(cheese._default_prefs, 'salads', ['pepper jack'])
    defaults_modified = cheese._default_prefs

    # 这里写入配置文件的字典已经是修改过后的
    cheese.write_default_cheese_preferences()

    # 读取配置文件中的字典信息
    actual = cheese.read_cheese_preferences()
    assert defaults_modified == actual
    assert defaults_modified != defaults_before
$ pytest -qs test_cheese.py::test_def_prefs_change_defaults
.
1 passed in 0.00s

当当当~测试完美通过。

比较常用的还有 chdir(path) 和 syspath_prepend(path) 。详细用法就不再这里赘述了,在上面简介 API 的地方有写。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值