如何在py.test中跨测试累积状态
我现在有一个项目和一些测试,跟这些差不多。
class mylib:
@classmethod
def get_a(cls):
return 'a'
@classmethod
def convert_a_to_b(cls, a):
return 'b'
@classmethod
def works_with(cls, a, b):
return True
class TestMyStuff(object):
def test_first(self):
self.a = mylib.get_a()
def test_conversion(self):
self.b = mylib.convert_a_to_b(self.a)
def test_a_works_with_b(self):
assert mylib.works_with(self.a, self.b)
在 py.test 0.9.2 版本中,这些测试(或者类似的测试)都能通过。但是在后来的版本中,test_conversion 和 test_a_works_with_b 这两个测试失败了,错误信息是 'TestMyStuff 没有属性 a'。
我猜这可能是因为在后来的 py.test 版本中,每个被测试的方法都会创建一个单独的 TestMyStuff 实例。
那么,正确的写法是什么呢?也就是说,怎么写这些测试才能在每个步骤中得到结果,同时又能使用之前(成功)测试的状态来进行后面的测试呢?
5 个回答
这肯定是pytest的“fixture”功能可以派上用场的地方:https://docs.pytest.org/en/latest/fixture.html
“Fixture”可以让测试函数轻松地获取和使用一些特定的、预先初始化好的应用对象,而不需要担心导入、设置和清理的细节。这是一个典型的依赖注入的例子,fixture函数充当注入者,而测试函数则是这些fixture对象的使用者。
下面是一个设置fixture来保存状态的例子:
import pytest
class State:
def __init__(self):
self.state = {}
@pytest.fixture(scope='session')
def state() -> State:
state = State()
state.state['from_fixture'] = 0
return state
def test_1(state: State) -> None:
state.state['from_test_1'] = 1
assert state.state['from_fixture'] == 0
assert state.state['from_test_1'] == 1
def test_2(state: State) -> None:
state.state['from_test_2'] = 2
assert state.state['from_fixture'] == 0
assert state.state['from_test_1'] == 1
assert state.state['from_test_2'] == 2
注意,你可以指定依赖注入的范围(也就是状态的范围)。在这个例子中,我把它设置为会话(session),另一个选项是module
(scope=function
不适合你的用例,因为这样会导致函数之间的状态丢失)。
显然,你可以扩展这个模式来保存其他类型的对象状态,比如比较不同测试的结果。
需要提醒的是——你仍然希望能够以任何顺序运行你的测试(我的例子中,如果把1和2的顺序调换就会失败)。不过为了简单起见,我没有展示这一点。
我部分同意Ned的看法,确实应该避免随意分享测试状态。但我也认为,有时候在测试过程中逐步积累状态是有用的。
使用py.test,你可以明确表示想要共享测试状态。下面是你例子修改后的版本,可以正常工作:
class State:
""" holding (incremental) test state """
def pytest_funcarg__state(request):
return request.cached_setup(
setup=lambda: State(),
scope="module"
)
class mylib:
@classmethod
def get_a(cls):
return 'a'
@classmethod
def convert_a_to_b(cls, a):
return 'b'
@classmethod
def works_with(cls, a, b):
return True
class TestMyStuff(object):
def test_first(self, state):
state.a = mylib.get_a()
def test_conversion(self, state):
state.b = mylib.convert_a_to_b(state.a)
def test_a_works_with_b(self, state):
mylib.works_with(state.a, state.b)
你可以在最近的py.test版本中运行这个。每个函数都会接收到一个“状态”对象,而“funcarg”工厂最开始会创建这个对象,并在模块范围内缓存它。再加上py.test保证测试是按照文件顺序运行的,这样测试函数就可以逐步处理这个测试“状态”。
不过,这种方法有点脆弱,因为如果你只运行“test_conversion”,比如用“py.test -k test_conversion”,那么测试会失败,因为第一个测试没有运行。我觉得有一种方法可以做增量测试会很好,所以也许我们最终能找到一个完全可靠的解决方案。
希望对你有帮助,
holger
好的单元测试实践是避免在测试之间积累状态。大多数单元测试框架都非常努力地防止你积累状态。原因是你希望每个测试都是独立的。这样,你可以随意选择测试的组合来运行,并且确保每次测试时系统都是干净的状态。