这篇文章接着上一篇《探索pytest的fixture(上)》的内容讲。
使用fixture函数的fixture
我们不仅可以在测试函数中使用fixture,而且fixture函数也可以使用其他fixture,这有助于fixture的模块化设计,并允许在许多项目中重新使用框架特定的fixture。例如,我们可以扩展前面的例子,并实例化一个app
对象,我们把已经定义好的smtp
资源粘贴到其中,新建一个test_appsetup.py文件,输入以下代码:
import pytest
class App(object):
def __init__(self, smtp):
self.smtp = smtp
@pytest.fixture(scope="module")
def app(smtp):
return App(smtp)
def test_smtp_exists(app):
assert app.smtp
在这里,我们声明一个app
fixture,用来接收之前定义的smtp
fixture,并用它实例化一个App
对象,让我们来运行它:
由于smtp
的参数化,测试将运行两次不同的App
实例和各自的smtp服务器。pytest将完全分析fixture依赖关系图,因此app
fixture不需要知道smtp
参数化。
还要注意一下的是,app
fixture具有一个module
(模块)范围,并使用module
(模块)范围的smtp
fixture。如果smtp
被缓存在一个session
(会话)范围内,这个例子仍然可以工作,fixture使用更大范围内的fixture是好的,但是不能反过来,session
(会话)范围的fixture不能以有意义的方式使用module
(模块)范围的fixture。
通过fixture实例自动分组测试
pytest在测试运行期间会最小化活动fixture的数量,如果我们有一个参数化的fixture,那么所有使用它的测试将首先执行一个实例,然后在下一个fixture实例被创建之前调用终结器。除此之外,这可以简化对创建和使用全局状态的应用程序的测试。
以下示例使用两个参数化fixture,其中一个作用于每个模块,所有功能都执行print
调用来显示设置流程,修改之前的test_module.py文件,输入以下代码:
import pytest
@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
param = request.param
print (" 设置 modarg %s" % param)
yield param
print (" 拆卸 modarg %s" % param)
@pytest.fixture(scope="function", params=[1,2])
def otherarg(request):
param = request.param
print (" 设置 otherarg %s" % param)
yield param
print (" 拆卸 otherarg %s" % param)
def test_0(otherarg):
print (" 用 otherarg %s 运行 test0" % otherarg)
def test_1(modarg):
print (" 用 modarg %s 运行 test1" % modarg)
def test_2(otherarg, modarg):
print (" 用 otherarg %s 和 modarg %s 运行 test2" % (otherarg, modarg))
让我们使用pytest -v -s test_module.py
运行详细模式测试并查看打印输出:
我们可以看到参数化的module
(模块)范围的modarg
资源影响了测试执行的排序,使用了最少的活动资源。mod1
参数化资源的终结器是在mod2
资源建立之前执行的。特别要注意test_0
是完全独立的,会首先完成,然后用mod1
执行test_1
,再然后用mod1
执行test_2
,再然后用mod2
执行test_1
,最后用mod2
执行test_2
。otherarg
参数化资源是function
(函数)的范围,是在每次使用测试之后建立起来的。
使用类、模块或项目的fixture
有时测试函数不需要直接访问一个fixture对象,例如,测试可能需要使用空目录作为当前工作目录,不关心具体目录。这里使用标准的tempfile和pytest fixture来实现它,我们将fixture的创建分隔成一个conftest.py文件:
import pytest
import tempfile
import os
@pytest.fixture()
def cleandir():
newpath = tempfile.mkdtemp()
os.chdir(newpath)
并通过usefixtures
标记声明在测试模块中的使用,新建一个test_setenv.py文件,输入以下代码:
import os
import pytest
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit(object):
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()) == []
由于使用了usefixtures
标记,所以执行每个测试方法都需要cleandir
fixture,就像为每个测试方法指定了一个cleandir
函数参数一样,让我们运行它来验证我们的fixture已激活:
我们可以像这样指定多个fixture:
@pytest.mark.usefixtures("cleandir", "anotherfixture")
我们可以使用标记机制的通用功能来指定测试模块级别的fixture使用情况:
pytestmark = pytest.mark.usefixtures("cleandir")
要注意的是,分配的变量必须被称为pytestmark
,例如,分配foomark
不会激活fixture。最后,我们可以将项目中所有测试所需的fixture放入一个pytest.ini
文件中:
[pytest]
usefixtures = cleandir
自动使用fixture
有时候,我们可能希望自动调用fixture,而不是显式声明函数参数或使用usefixtures
装饰器,例如,我们有一个数据库fixture,它有一个开始、回滚、提交的体系结构,我们希望通过一个事务和一个回滚自动地包含每一个测试方法,新建一个test_db_transact.py文件,输入以下代码:
import pytest
class DB(object):
def __init__(self):
self.intransaction = []
def begin(self, name):
self.intransaction.append(name)
def rollback(self):
self.intransaction.pop()
@pytest.fixture(scope="module")
def db():
return DB()
class TestClass(object):
@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"]
类级别的transact
fixture被标记为autouse=true
,这意味着类中的所有测试方法将使用该fixture,而不需要在测试函数签名或类级别的usefixtures
装饰器中陈述它。如果我们运行它,会得到两个通过的测试:
以下是在其他范围内如何使用自动fixture:
- 自动fixture遵循
scope=
关键字参数,如果一个自动fixture的scope='session'
,它将只运行一次,不管它在哪里定义。scope='class'
表示每个类会运行一次,等等。 - 如果在一个测试模块中定义一个自动fixture,所有的测试函数都会自动使用它。
- 如果在
conftest.py
文件中定义了自动fixture,那么在其目录下的所有测试模块中的所有测试都将调用fixture。 - 最后,要小心使用自动fixture,如果我们在插件中定义了一个自动fixture,它将在插件安装的所有项目中的所有测试中被调用。例如,在
pytest.ini
文件中,这样一个全局性的fixture应该真的应该做任何工作,避免无用的导入或计算。
最后还要注意,上面的的transact
fixture可能是我们希望在项目中提供的fixture,而没有通常的激活,规范的方法是将定义放在conftest.py
文件而不使用自动运行:
@pytest.fixture
def transact(request, db):
db.begin()
yield
db.rollback()
然后例如,有一个测试类通过声明使用它需要:
@pytest.mark.usefixtures("transact")
class TestClass(object):
def test_method1(self):
......
在这个测试类中的所有测试方法将使用transact
fixture,而模块中的其他测试类或函数将不会使用它,除非它们也添加一个transact
引用。
覆盖不同级别的fixture
在相对较大的测试套件中,我们很可能需要使用本地定义的套件重写全局或根fixture,从而保持测试代码的可读性和可维护性。
覆盖文件夹级别的fixture
鉴于测试文件的结构是:
tests/
__init__.py
conftest.py
# tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# tests/test_something.py
def test_username(username):
assert username == 'username'
subfolder/
__init__.py
conftest.py
# tests/subfolder/conftest.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
test_something.py
# tests/subfolder/test_something.py
def test_username(username):
assert username == 'overridden-username'
正如上面代码所示,具有相同名称的fixture可以在某些测试文件夹级别上被覆盖,但是要注意的是,base
或super
fixture可以轻松地从上面的fixture进入,并在上面的例子中使用。
覆盖测试模块级别的fixture
鉴于测试文件的结构是:
tests/
__init__.py
conftest.py
# tests/conftest.py
@pytest.fixture
def username():
return 'username'
test_something.py
# 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
# 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。
直接用测试参数化覆盖fixture
鉴于测试文件的结构是:
tests/
__init__.py
conftest.py
# tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
@pytest.fixture
def other_username(username):
return 'other-' + username
test_something.py
# 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'
在上面的例子中,fixture值被测试参数值覆盖,即使测试不直接使用它,fixture的值也可以用这种方式重写。
使用非参数化参数替代参数化的fixture
鉴于测试文件的结构是:
tests/
__init__.py
conftest.py
# 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
# 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
# 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被某个测试模块的参数化版本覆盖,这同样适用于测试文件夹级别。