目录
什么是固件
固件(Fixture)是一些函数,pytest 会在执行测试函数之前或之后加载运行它们。
可以利用固件做很多事情,最常见的就是数据库的初始连接和最后关闭操作。
举个例子:
使用 pytest.fixture() 定义固件,只返回北京邮编
# test_postcode.py
import pytest
@pytest.fixture()
def postcode():
return '010'
def test_postcode(postcode):
assert postcode == '010'
if __name__ == '__main__':
pytest.main('-s test_postcode.py'.split())
运行结果:
============================= test session starts =============================
platform win32 -- Python 3.9.2, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\Administrator\Desktop\test\20210407\test_postcode
collected 1 item
test_postcode.py .
============================== 1 passed in 0.02s ==============================
***Repl Closed***
固件可以直接定义在各测试脚本中,就像上面的例子。更多时候,我们希望一个固件可以在更大程度上复用,这就需要对固件进行集中管理。Pytest 使用文件 conftest.py
集中管理固件。
注意:
不要自己显式调用 conftest.py,pytest 会自动调用,可以把 conftest 当做插件来理解。
在复杂的项目中,可以在不同的目录层级定义 conftest.py,其作用域为其所在的目录和子目录。
预处理和后处理
当大量重复预处理(数据库建立)和后处理(测试完成进行清理)这类操作,最佳方法是使用固件来自动化所有预处理和后处理。
Pytest 使用 yield
关键词将固件分为两部分,yield 之前的代码属于预处理,会在测试前执行;yield 之后的代码属于后处理,将在测试完成后执行。
以下测试模拟数据库查询,使用固件来模拟数据库的连接和关闭:
# test_db.py
import pytest
@pytest.fixture()
def db():
print('Connection successful')
yield
print('Connection closed')
def search_user(user_id):
d = {
'001': 'xiaoming'
}
return d[user_id]
def test_search(db):
assert search_user('001') == 'xiaoming'
if __name__ == '__main__':
pytest.main('-s test_db.py'.split())
这里使用 -s 选项,阻止消息被吞
运行结果:
============================= test session starts =============================
platform win32 -- Python 3.9.2, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\Administrator\Desktop\test\20210407\test_db
collected 1 item
test_db.py Connection successful
.Connection closed
============================== 1 passed in 0.01s ==============================
***Repl Closed***
从结果可以看到,测试成功的 .
标识前后有数据库的连接和关闭操作。
如果想要更细的跟踪固件操作,可以使用 --setup-show
选项。
作用域
固件的作用是为了抽离重复的工作和方便复用,为了更精细化控制固件,比如只想对数据库访问测试脚本使用自动连接关闭的固件,pytest 使用作用域来进行指定固件的使用范围。
定义固件时,通过 scope 参数声明作用域,可选项有:
function
:函数级,每个测试函数都会执行一次固件class
:类级别,每个测试类执行一次,所有方法都可以使用module
:模块级,每个模块执行一次,模块内函数和方法都可使用session
:会话级,一次测试只执行一次,所有被找到的函数和方法都可用
默认的作用域为 function
# test_scope.py
import pytest
@pytest.fixture(scope='function')
def func_scope():
pass
@pytest.fixture(scope='module')
def mod_scope():
pass
@pytest.fixture(scope='session')
def sess_scope():
pass
@pytest.fixture(scope='class')
def class_scope():
pass
# 最简单使用固件方式是作为测试函数参数
def test_multi_scope(sess_scope,mod_scope,func_scope):
pass
if __name__ == '__main__':
pytest.main('--setup-show test_scope.py'.split())
可清楚看到各固件的作用域和执行顺序
对于类使用作用域,需要使用 pytest.mark.usefixtures
(对函数和方法也适用):
# test_scope.py
import pytest
@pytest.fixture(scope='class')
def class_scope():
pass
@pytest.mark.usefixtures('class_scope')
class TestClassScope:
def test_1(self):
pass
def test_2(self):
pass
if __name__ == '__main__':
pytest.main('--setup-show test_scope.py::TestClassScope'.split())
从结果可以看到,所有测试函数都在固件作用范围内
自动执行
从学习入门到现在,所有的固件的使用都是手动指定,或者作为参数,或者使用 usefixtures
如果想要固件自动执行,可以在定义时指定 autouse
参数
下面是两个自动计时固件,一个用于统计每个函数运行时间(function 作用域),一个用于计算测试总耗时(session 作用域):
# test_autouse.py
import pytest
import time
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
@pytest.fixture(scope='session', autouse=True)
def timer_session_scope():
start = time.time()
print('\nstart: {}'.format(time.strftime(DATE_FORMAT, time.localtime(start))))
yield
finished = time.time()
print('finished: {}'.format(time.strftime(DATE_FORMAT,time.localtime(finished))))
print('Total time cost: {:.3f}'.format(finished-start))
@pytest.fixture(autouse=True)
def timer_function_scope():
start = time.time()
yield
print(' Time cost: {:.3f}s'.format(time.time()-start))
# 无显式使用固件
def test_1():
time.sleep(1)
def test_2():
time.sleep(2)
if __name__ == '__main__':
pytest.main('-s test_autouse.py'.split())
执行结果为:
固件自动执行并完成计时任务
重命名
固件的名称默认为定义时的函数名,如果不想使用默认,可以通过 name 选项指定名称
# test_rename.py
@pytest.fixture(name='age')
def calculate_average_age():
return 28
def test_age(age):
assert age == 28
参数化
因为固件也是函数,所以也能进行参数化。
假设现在有一批 API 需要测试对不同数据库的支持情况(对所有数据库进行相同操作),最简单的方法就是针对每个数据库编写一个测试用例,但这包含大量重复代码,如数据库的连接、关闭,查询等。
进一步,可以使用固件抽离出数据库的通用操作,每个 API 都能复用这些数据库固件,同时可维护性也得到提升。
更进一步,可以继续将这些固件合并为一个,而通过参数控制连接到不同的数据库。这就需要使用固件参数化来实现。
固件参数化需要使用 pytest 内置的固件 request
,并通过 request.param
获取参数。
# test_fixture_param.py
import pytest
@pytest.fixture(params=[
('redis', '6379'),
('elasticsearch','9200')
])
def param(request):
return request.param
@pytest.fixture(autouse=True)
def db(param):
print('\nSucceed to connect %s:%s' % param)
yield
print('\nSucceed to close %s:%s' % param)
def test_api():
assert 1 == 1
if __name__ == '__main__':
pytest.main('-s test_fixture_param.py'.split())
注意:
与函数参数化使用 @pytest.mark.parametrize 不同,固件在定义时使用 params
参数进行参数化。
固件参数化依赖于内置固件 request
及其属性 param
。
内置固件
tmpdir & tmpdir_factory
用于临时文件和目录管理,默认会在测试结束时删除。
tmpdir
注意:
tmpdir 只有 function 作用域,只能在函数内使用。
使用 tmpdir.mkdir()
创建临时目录,tmpdir.join()
创建临时文件(或者使用创建的目录)。
# test_tmpdir.py
import pytest
def test_tmpdir(tmpdir):
a_dir = tmpdir.mkdir('mytmpdir')
a_file = a_dir.join('tmpfile.txt')
a_file.write('hello, pytest!')
assert a_file.read() == 'hello, pytest!'
if __name__ == '__main__':
pytest.main('test_tmpdir.py'.split())
tmpdir_factory
tmpdir_factory 可以在所有作用域使用,包括 function, class, module, session
举例:
# test_tmpdir.py
import pytest
@pytest.fixture(scope='module')
def my_tmpdir_factory(tmpdir_factory):
a_dir = tmpdir_factory.mktemp('mytmpdir')
a_file = a_dir.join('tmpfile.txt')
a_file.write('hello, pytest!')
return a_file
pytestconfig
使用 pytestconfig
,可以很方便的读取命令行参数和配置文件。
举例:
capsys
capsys
用于捕获 stdout
和 stderr
的内容,并临时关闭系统输出。
monkeypatch
monkeypath
用于运行时动态修改类或模块。
recwarn
recwarn
用于捕获程序中 warnings 产生的警告。
下面代码里的 warning 没有定义。还没找到解决办法。
# test_recwarn.py
def warn():
warnings.warn('Deprecated function', DeprecationWarning)
def test_warn(recwarn):
warn()
assert len(recwarn) == 1
w = recwarn.pop()
assert w.category == DeprecationWarning