上一篇文章已经介绍了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 的地方有写。