如何在py.test中跨模块共享变量供所有测试使用
我有很多测试是用py.test运行的,这些测试分布在多个类和多个文件里。
我想找个最简单的方法,把一个很大的字典分享给每个类里的每个方法,这样py.test就能用到这个字典。我不想重复这个字典,所以我希望它能在所有地方都能用。
简单来说,我需要为每个测试创建一个“全局变量”。在py.test之外,我并不需要这个变量,所以我不想把它存储在被测试的文件里。我之前常用py.test的fixtures(固定装置),但我觉得这对我现在的需求来说有点过于复杂。也许这是唯一的办法?
5 个回答
你可以在 pytest_addoption
这个钩子里添加你的全局变量作为一个选项。你可以选择用 addoption
明确地添加,或者如果你希望这个属性不需要检查命令行就能确定,可以使用 set_defaults
方法,具体可以参考这个链接 docs。
当你定义了选项后,可以在任何一个夹具(fixture)里使用 request.config.getoption
来获取这个选项,然后可以明确地把它传递给测试,或者使用自动使用(autouse)。另外,你几乎可以把你的选项传递到 config
对象里的任何钩子中。
#conftest.py
def pytest_addoption(parser):
parser.addoption("--my_global_var", default="foo")
parser.set_defaults(my_hidden_var="bar")
@pytest.fixture()
def my_hidden_var(request):
return request.config.getoption("my_hidden_var")
#test.py
def test_my_hidden_var(my_hidden_var):
assert my_hidden_var == "bar"
在每个测试中使用一个很大的全局字典可能不是个好主意。如果可以的话,我建议你重新组织一下测试,尽量避免这种情况。
不过,假如你真的需要这样做,我会这样处理:定义一个自动使用的固定装置,这样每个函数都能在全局命名空间中引用这个字典。
下面是一些代码。虽然这些代码都在同一个文件里,但你可以把固定装置移到测试的顶层文件conftest.py
中。
import pytest
my_big_global = {'key': 'value'}
@pytest.fixture(autouse=True)
def myglobal(request):
request.function.func_globals['foo'] = my_big_global
def test_foo():
assert foo['key'] == 'value'
def test_bar():
assert foo['key'] == 'bar'
这是我运行这段代码时的输出:
$ py.test test_global.py -vv
======================================= test session starts =======================================
platform darwin -- Python 2.7.5 -- py-1.4.20 -- pytest-2.5.2 -- env/bin/python
collected 2 items
test_global.py:9: test_foo PASSED
test_global.py:12: test_bar FAILED
============================================ FAILURES =============================================
____________________________________________ test_bar _____________________________________________
def test_bar():
> assert foo['key'] == 'bar'
E assert 'value' == 'bar'
E - value
E + bar
test_global.py:13: AssertionError
=============================== 1 failed, 1 passed in 0.01 seconds ===============================
要注意的是,你不能使用会话范围的固定装置,因为那样你就无法访问每个函数对象。因此,我确保只定义一次我的大全局字典,并使用对它的引用——如果我在赋值语句中定义字典,每次都会生成一个新的副本。
最后,做这样的事情可能不是个好主意。祝你好运 :)
我很惊讶之前没有人提到缓存这个话题:自从2.8版本开始,pytest
就有了一个强大的缓存机制。
使用示例
@pytest.fixture(autouse=True)
def init_cache(request):
data = request.config.cache.get('my_data', None)
data = {'spam': 'eggs'}
request.config.cache.set('my_data', data)
在测试中通过内置的request
工具获取数据字典:
def test_spam(request):
data = request.config.cache.get('my_data')
assert data['spam'] == 'eggs'
在测试运行之间共享数据
request.cache
的一个很酷的地方是它会保存在硬盘上,这样可以在不同的测试运行之间共享。这在你使用分布式测试(pytest-xdist
)或者有一些生成的数据在生成后不会改变时特别有用:
@pytest.fixture(autouse=True)
def generate_data(request):
data = request.config.cache.get('my_data', None)
if data is None:
data = long_running_generation_function()
request.config.cache.set('my_data', data)
现在,测试在不同的运行中不需要重新计算值,除非你明确地清除硬盘上的缓存。你可以查看当前缓存中的内容:
$ pytest --cache-show
...
my_data contains:
{'spam': 'eggs'}
使用--cache-clear
标志重新运行测试,这样可以删除缓存并强制重新计算数据。或者直接删除项目根目录下的.pytest_cache
文件夹。
接下来该怎么做
有关内容的更多信息,可以查看pytest
文档中的相关部分:缓存:处理跨测试运行的状态。
我非常喜欢py.test的很多功能,但有一点我真的很讨厌,那就是它和代码智能工具的兼容性太差。我不同意用自动使用的fixture来声明一个变量是“最清晰”的方法,因为这不仅让我的代码检查工具感到困惑,也会让那些不熟悉py.test的人感到迷惑。在我看来,这里面有很多“魔法”。
所以,有一种方法可以避免让你的代码检查工具崩溃,也不需要写很多重复的测试代码,那就是创建一个叫做globals的模块。在这个模块里,你可以把想要全局使用的变量名设置为{}或者None,然后在你的测试中导入这个globals模块。接着在你的conftest.py文件里,利用py.test的钩子来适当地设置(或重置)你的全局变量。这种方法的好处是,你在构建测试时可以使用这些变量的占位符,而在运行测试时又能获取到完整的数据。
举个例子,你可以使用pytest_configure()
钩子在py.test启动时就设置好你的字典。或者,如果你想确保每个测试之间数据都是干净的,你可以使用自动的fixture在每个测试之前把全局变量赋值为你已知的状态。
# globals.py
my_data = {} # Create a stub for your variable
# test_module.py
import globals as gbl
def test_foo():
assert gbl.my_data['foo'] == 'bar' # The global is in the namespace when creating tests
# conftest.py
import globals as gbl
my_data = {'foo': 'bar'} # Create the master copy in conftest
@pytest.fixture(autouse=True)
def populate_globals():
gbl.my_data = my_data # Assign the master value to the global before each test
这种方法的另一个好处是,你可以在globals模块中使用类型提示,这样在测试中使用全局对象时就能享受到代码补全的功能。虽然对于字典来说这可能不是必需的,但我觉得在使用对象(比如webdriver)时,这个功能非常方便。:)
更新:pytest-namespace这个功能已经被弃用/移除,请不要使用。 详情请查看#3735。
你提到的最简单且不复杂的选择就是使用“fixture”(固定装置)。你可以通过在模块中使用 pytestmark = pytest.mark.usefixtures('big_dict')
来将它应用到整个模块,但这样的话它就不在你的命名空间里,所以如果你想明确地使用它,可能还是直接请求它比较好。
另外,你也可以通过钩子将东西放入pytest的命名空间:
# conftest.py
def pytest_namespace():
return {'my_big_dict': {'foo': 'bar'}}
这样你就可以使用 pytest.my_big_dict
了。不过,使用fixture可能还是更方便一些。