pytest fixtures: explicit, modular, scalable¶
测试装置帮助搭建测试平台,满足不同测试用例的测试要求。在pytest中,测试装置是通过函数参数的形式传给测试用例函数。
pytest的测试装置机制比xUnit的更加灵活,但是于此同时,pytest也兼容xUnit风格的测试装置搭建。
测试装置通过@pytest.fixture
修饰符来定义。下面是pytest自带的测试装置:
- capfd,捕捉文本输出到文件描述符1和2
- capfdbinay,捕捉二进制输出文件描述符1和2
- caplog,控制跟踪记录
- capsys,捕捉文本输出到sys.stdout和sys.stderr
- capsysbinday, 同上,但输出二进制
- cache,缓存运行时候的值
- doctest_namespace,把一个字典注入到doctests的命名空间
- monkeypatch,临时修改类型、函数、字典、os.environ以及其他
- pytestconfig,访问pytest的配置信息、插件管理器以及插件钩子
- record_property,为测试用例添加额外的部属
- record_testsuite_proper,为测试套装添加额外的部属
- recwarn,记录测试用例产生的告警信息
- request,提供测试用例的信息
- testdir,提供一个临时的目录供测试用例允许
- tmp_path, 为测试用例提供一个唯一的pathlib.path目录对象
- temp_path_factory,在会话范围提供临时的pathlib.path目录
- tmpdir, 被tmp_path替代了,原本的用意是为测试用例提供一个唯一的py.path.local对象
- tmpdir_factory,被temp_path_factory替代了
Fixtures as Function arguments
使用测试装置的一个例子:
# content of ./test_smtpsimple.py
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
通过pytest --fixtures test_simplefactory.py
可以查看脚本中提供的测试装置
测试装置可以用来做测试注入(https://en.wikipedia.org/wiki/Dependency_injection)。也就是把测试装置对象作为被测对象。
在conftest.py
中书写的测试装置会被自动识别并导入。pytest会先查找测试用例,然后是测试模块,然后是conftest.py,然后是自带的或者第三方插件。不同目录可以指定不同的conftest.py来自定义测试。
测试装置可以用来在不同用例间分享测试数据。另一个办法是把测试数据放在tests目录。pytest-datadir和pytest-datafiles可以帮助你组织测试数据。
如果想在测试模块范围内使用同一个测试装置,可以在@pytest.fixture上使用scope=“module"参数:
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
scope参数的取值可以是function, class, module, package或者是session。
pytest 5.2开始允许动态决定scope的值:
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers", None):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def docker_container():
yield spawn_container()
范围越大的装置越早被初始化。范围相同的装置按照定义顺序来初始化。Autouse的装置在显示使用的装置之前初始化。
在装置中使用yield语句的话,yield语句之后的代码会被作为teardown代码:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
yield smtp_connection # provide the fixture value
print("teardown smtp")
smtp_connection.close()
也可以在with语句中使用yield:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
yield smtp_connection # provide the fixture value
如果setup代码出异常,那么yield语句以后的代码不会被执行。但是无论setup代码是否抛出异常,contextlib.ExitStack 的清理函数保证会被调用:
import contextlib
import pytest
@contextlib.contextmanager
def connect(port):
... # create connection
yield
... # close connection
@pytest.fixture
def equipments():
with contextlib.ExitStack() as stack:
yield [stack.enter_context(connect(port)) for port in ("C1", "C3", "C28")]
另一种teardown的方法是使用request-context的addfinalizer:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection(request):
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def fin():
print("teardown smtp_connection")
smtp_connection.close()
request.addfinalizer(fin)
return smtp_connection # provide the fixture value
测试装置可以通过request参数来检视测试用例信息:
@pytest.fixture(scope="module")
def smtp_connection(request):
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp_connection = smtplib.SMTP(server, 587, timeout=5)
yield smtp_connection
print("finalizing {} ({})".format(smtp_connection, server))
smtp_connection.close()
测试装置还可以返回一个对象工厂,下面是一个例子:
@pytest.fixture
def make_customer_record():
created_records = []
def _make_customer_record(name):
record = models.Customer(name=name, orders=[])
created_records.append(record)
return record
yield _make_customer_record
for record in created_records:
record.destroy()
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
可以对测试装置进行参数化,这样这个测试装置会被调用多次:
import pytest
import smtplib
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_connection
print("finalizing {}".format(smtp_connection))
smtp_connection.close()
上面的装置会有两个版本,一个是以smtp.gmail.com为参数;另一个是mail.python.org。如果有一个测试用例test_ehlo使用到了这个fixture,那么这个用例也会有两份,一个test_ehlo[smtp.gmail.com]
,另一个是test_ehlo[mail.python.org]
。通过pytest的--collect-only
选项可以查看生成的名字。
test ID会被处理成字符串形式。Numbers, strings, booleans以及None都会被转为相应的字符串形式。也可以通过ids来显示指定其他ID:
import pytest
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
return request.param
def test_a(a):
pass
def idfn(fixture_value):
if fixture_value == 0:
return "eggs"
else:
return None
@pytest.fixture(params=[0, 1], ids=idfn)
def b(request):
return request.param
def test_b(b):
pass
上面的例子还展示了ids是函数的情况。
使用pytest.param()可以往参数化的测试装置上添加mark:
import pytest
@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
def data_set(request):
return request.param
def test_data(data_set):
pass
在一个测试装置里面可以引用另一个测试装置:
import pytest
class App:
def __init__(self, smtp_connection):
self.smtp_connection = smtp_connection
@pytest.fixture(scope="module")
def app(smtp_connection):
return App(smtp_connection)
def test_smtp_connection_exists(app):
assert app.smtp_connection
pytest会保证创建尽量少的测试装置实例。的测试装置用完就会被回收。
一个测试用例可能只需要知道一个测试装置存在,但并不需要直接使用这个装置,这种情况可以使用usefixtures标记:
import os
import pytest
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
def test_cwd_starts_empty(self):
assert os.listdir(os.getcwd()) == []
with open("myfile", "w") as f:
f.write("hello")
def test_cwd_again_starts_empty(self):
assert os.listdir(os.getcwd()) == []
也可以在pytest.ini中指定:
# content of pytest.ini
[pytest]
usefixtures = cleandir
注意usefixtures不能再测试装置上使用。
可以指定自动使用某个测试装置:
class TestClass:
@pytest.fixture(autouse=True)
def transact(self, request, db):
db.begin(request.function.__name__)
yield
db.rollback()
def test_method1(self, db):
assert db.intransaction == ["test_method1"]
def test_method2(self, db):
assert db.intransaction == ["test_method2"]
使用autouse有一些规则需要遵守,章节中有介绍。
一个小范围的测试装置可以覆盖以及访问同名的大范围的测试装置。
(本篇完)