目录:
- 安装及入门
- 使用和调用方法
- 原有TestSuite使用方法
- 断言的编写和报告
- Pytest fixtures:清晰 模块化 易扩展
- 使用Marks标记测试用例
- Monkeypatching/对模块和环境进行Mock
- 使用tmp目录和文件
- 捕获stdout及stderr输出
- 捕获警告信息
- 模块及测试文件中集成doctest测试
- skip及xfail: 处理不能成功的测试用例
- Fixture方法及测试用例的参数化
- 缓存: 使用跨执行状态
- unittest.TestCase支持
- 运行Nose用例
- 经典xUnit风格的setup/teardown
- 安装和使用插件
- 插件编写
- 编写钩子(hook)方法
- 运行日志
- API参考
- 优质集成实践
- 片状测试
- Pytest导入机制及sys.path/PYTHONPATH
- 配置选项
- 示例及自定义技巧
- Bash自动补全设置
Pytest fixtures:清晰 模块化 易扩展
2.0/2.3/2.4版本新功能
text fixtures的目的是为测试的重复执行提供一个可靠的固定基线。 pytest fixture比经典的xUnit setUp/tearDown方法有着显着的改进:
- fixtures具有明确的名称,在测试方法/类/模块或整个项目中通过声明使用的fixtures名称来使用。
- fixtures以模块化方式实现,因为每个fixture名称都会触发调用fixture函数,该fixture函数本身可以使用其它的fixtures。
- 从简单的单元测试到复杂的功能测试,fixtures的管理允许根据配置和组件选项对fixtures和测试用例进行参数化,或者在测试方法/类/模块或整个测试会话范围内重复使用该fixture。
此外,pytest继续支持经典的xUnit风格的setup方法。 你可以根据需要混合使用两种样式,逐步从经典样式移动到新样式。 您也可以从现有的unittest.TestCase样式或基于nose的项目开始。
Fixtures作为函数参数使用
测试方法可以通过在其参数中使用fixtures名称来接收fixture对象。 每个fixture参数名称所对应的函数,可以通过使用@pytest.fixture
注册成为一个fixture函数,来为测试方法提供一个fixture对象。 让我们看一个只包含一个fixture和一个使用它的测试方法的简单独立测试模块:
# ./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
这里,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-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile:
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:11: AssertionError
========================= 1 failed in 0.12 seconds =========================
在测试失败的回溯信息中,我们看到测试方法是使用smtp_connection
参数调用的,即由fixture函数创建的smtplib.SMTP()
实例。测试用例在我们故意的assert 0
上失败。以下是pytest用这种方式调用测试方法使用的确切协议:
Fixtures: 依赖注入的主要例子
Fixtures允许测试方法能轻松引入预先定义好的初始化准备函数,而无需关心导入/设置/清理方法的细节。 这是依赖注入的一个主要示例,其中fixture函数的功能扮演”注入器“的角色,测试方法来“消费”这些fixture对象。
conftest.py: 共享fixture函数
如果在测试中需要使用多个测试文件中的fixture函数,则可以将其移动到conftest.py
文件中,所需的fixture对象会自动被pytest
发现,而不需要再每次导入。 fixture函数的发现顺序从测试类开始,然后是测试模块,然后是conftest.py文件,最后是内置和第三方插件。
你还可以使用conftest.py文件来实现本地每个目录的插件。
共享测试数据
如果要使用数据文件中的测试数据,最好的方法是将这些数据加载到fixture函数中以供测试方法注入使用。这利用到了pytest
的自动缓存机制。
另一个好方法是在tests文件夹中添加数据文件。 还有社区插件可用于帮助处理这方面的测试,例如:pytest-datadir
和pytest-datafiles
。
生效范围:在测试类/测试模块/测试会话中共享fixture对象
由于fixtures对象需要连接形成依赖网,而通常创建时间比较长。 扩展前面的示例,我们可以在@pytest.fixture
调用中添加scope ="module"
参数,以使每个测试模块只调用一次修饰的smtp_connection
fixture函数(默认情况下,每个测试函数调用一次)。 因此,测试模块中的多个测试方法将各自注入相同的smtp_connection
fixture对象,从而节省时间。scope参数的可选值包括:function(函数), class(类), module(模块), package(包)及 session(会话)。
下一个示例将fixture函数放入单独的conftest.py文件中,以便来自目录中多个测试模块的测试可以访问fixture函数:
# conftest.py文件内容
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
fixture对象的名称依然是smtp_connection
,你可以通过在任何测试方法或fixture函数(在conftest.py所在的目录中或下面)使用参数smtp_connection
作为输入参数来访问其结果:
# 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 test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
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:6: 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:11: AssertionError
========================= 2 failed in 0.12 seconds =========================
你会看到两个assert 0
失败信息,更重要的是你还可以看到相同的(模块范围的)smtp_connection
对象被传递到两个测试方法中,因为pytest在回溯信息中显示传入的参数值。 因此,使用smtp_connection
的两个测试方法运行速度与单个函数一样快,因为它们重用了相同的fixture对象。
如果您决定要使用session(会话,一次运行算一次会话)范围的smtp_connection
对象,则只需如下声明:
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests needing it
...
最后,class(类)范围将为每个测试类调用一次fixture对象。
注意:
Pytest一次只会缓存一个fixture实例。 这意味着当使用参数化fixture时,pytest可能会在给定范围内多次调用fixture函数。
package(包)范围的fixture(实验性功能)
3.7版本新功能
在pytest 3.7中,引入了包范围。 当包的最后一次测试结束时,最终确定包范围的fixture函数。
警告:
此功能是实验性的,如果在获得更多使用后发现隐藏的角落情况或此功能的严重问题,可能会在将来的版本中删除。
谨慎使用此新功能,请务必报告您发现的任何问题。
高范围的fixture函数优先实例化
3.5版本新功能
在测试函数的fixture对象请求中,较高范围的fixture(例如session会话级)较低范围的fixture(例如function函数级或class类级优先执行。相同范围的fixture对象的按引入的顺序及fixtures之间的依赖关系按顺序调用。
请考虑以下代码:
@pytest.fixture(scope="session")
def s1():
pass
@pytest.fixture(scope="module")
def m1():
pass
@pytest.fixture
def f1(tmpdir):
pass
@pytest.fixture
def f2():
pass
def test_foo(f1, m1, f2, s1):
...
test_foo
中fixtures将按以下顺序执行:
- s1:是最高范围的fixture(会话级)
- m1:是第二高的fixture(模块级)
- tmpdir:是一个函数级的fixture,f1依赖它,因此它需要在f1前调用
- f1:是test_foo参数列表中第一个函数范围的fixture。
- f2:是test_foo参数列表中最后一个函数范围的fixture。
fixture结束/执行teardown代码
当fixture超出范围时,通过使用yield语句而不是return,pytest支持fixture执行特定的teardown代码。yield语句之后的所有代码都视为teardown代码:
# 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()
无论测试的异常状态如何,print
和smtp.close()
语句将在模块中的最后一个测试完成执行时执行。
让我们执行一下(上文的test_module.py
):
$ pytest -s -q --tb=no
FFteardown smtp
2 failed in 0.12 seconds
我们看到smtp_connection
实例在两个测试完成执行后完成。 请注意,如果我们使用scope ='function'
修饰我们的fixture函数,那么每次单个测试都会进行fixture的setup和teardown。 在任何一种情况下,测试模块本身都不需要改变或了解fixture函数的这些细节。
请注意,我们还可以使用with语句无缝地使用yield语法:
# test_yield2.py文件内容
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
测试结束后, smtp_connection
连接将关闭,因为当with语句结束时,smtp_connection
对象会自动关闭。
请注意,如果在设置代码期间(yield关键字之前)发生异常,则不会调用teardown代码(在yield之后)。
执行teardown代码的另一种选择是利用请求上下文对象的addfinalizer
方法来注册teardown函数。
以下是smtp_connection
fixture函数更改为使用addfinalizer
进行teardown:
# content of conftest.py
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
yield
和addfinalizer
方法在测试结束后调用它们的代码时的工作方式类似,但addfinalizer
相比yield
有两个主要区别:
- 使用
addfinalizer
可以注册多个teardown功能。 - 无论fixture中setup代码是否引发异常,都将始终调用teardown代码。 即使其中一个资源无法创建/获取,也可以正确关闭fixture函数创建的所有资源:
@pytest.fixture
def equipments(request):
r = []
for port in ('C1', 'C3', 'C28'):
equip = connect(port)
request.addfinalizer(equip.disconnect)
r.append(equip)
return r
在上面的示例中,如果“C28”因异常而失败,则“C1”和“C3”仍将正确关闭。 当然,如果在注册finalize
函数之前发生异常,那么它将不会被执行。
Fixtures中使用测试上下文的内省信息
Fixtures工厂方法
Fixtures参数化
使用参数化fixtures标记
模块化:在fixture函数中使用fixtures功能
使用fixture实例自动组织测试用例
在类/模块/项目中使用fixtures
自动使用fixtures(xUnit 框架的setup固定方法)
不同级别的fixtures的覆盖(优先级)
相对于在较大范围的测试套件中的Test Fixtures方法,在较小范围子套件你可能需要重写和覆盖外层的Test Fixtures方法,从而保持测试代码的可读性和可维护性。
在文件夹级别(通过conftest文件)重写fixtures方法
假设用例目录结构为:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
def test_username(username):
assert username == 'username'
subfolder/
__init__.py
conftest.py
# content of tests/subfolder/conftest.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
test_something.py
# content of tests/subfolder/test_something.py
def test_username(username):
assert username == 'overridden-username'
你可以看到, 基础/上级fixtures方法可以通过子文件夹下的con
ftest.py中同名的fixtures方法覆盖, 非常简单, 只需要按照上面的例子使用即可.
在测试模块级别重写fixtures方法
假设用例文件结构如下:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
def test_username(username):
assert username == 'overridden-username'
test_something_else.py
# content of tests/test_something_else.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-else-' + username
def test_username(username):
assert username == 'overridden-else-username'
上面的例子中, 用例模块(文件)中的fixture方法会覆盖文件夹conftest.py中同名的fixtures方法
在直接参数化方法中覆盖fixtures方法
假设用例文件结构为:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
@pytest.fixture
def other_username(username):
return 'other-' + username
test_something.py
# content of tests/test_something.py
import pytest
@pytest.mark.parametrize('username', ['directly-overridden-username'])
def test_username(username):
assert username == 'directly-overridden-username'
@pytest.mark.parametrize('username', ['directly-overridden-username-other'])
def test_username_other(other_username):
assert other_username == 'other-directly-overridden-username-other'
在上面的示例中,username fixture方法的结果值被参数化值覆盖。 请注意,即使测试不直接使用(也未在函数原型中提及),也可以通过这种方式覆盖fixture的值。
使用非参数化fixture方法覆盖参数化fixtures方法, 反之亦然
假设用例结构为:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture(params=['one', 'two', 'three'])
def parametrized_username(request):
return request.param
@pytest.fixture
def non_parametrized_username(request):
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def parametrized_username():
return 'overridden-username'
@pytest.fixture(params=['one', 'two', 'three'])
def non_parametrized_username(request):
return request.param
def test_username(parametrized_username):
assert parametrized_username == 'overridden-username'
def test_parametrized_username(non_parametrized_username):
assert non_parametrized_username in ['one', 'two', 'three']
test_something_else.py
# content of tests/test_something_else.py
def test_username(parametrized_username):
assert parametrized_username in ['one', 'two', 'three']
def test_username(non_parametrized_username):
assert non_parametrized_username == 'username'
在上面的示例中,使用非参数化fixture方法覆盖参数化fixture方法,以及使用参数化fixture覆盖非参数化fixture以用于特定测试模块。 这同样适用于文件夹级别的fixtures方法。