如何在py.test中跨测试累积状态

9 投票
5 回答
8223 浏览
提问于 2025-04-16 00:36

我现在有一个项目和一些测试,跟这些差不多。

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 个回答

8

这肯定是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),另一个选项是modulescope=function不适合你的用例,因为这样会导致函数之间的状态丢失)。

显然,你可以扩展这个模式来保存其他类型的对象状态,比如比较不同测试的结果。

需要提醒的是——你仍然希望能够以任何顺序运行你的测试(我的例子中,如果把1和2的顺序调换就会失败)。不过为了简单起见,我没有展示这一点。

10

我部分同意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

9

好的单元测试实践是避免在测试之间积累状态。大多数单元测试框架都非常努力地防止你积累状态。原因是你希望每个测试都是独立的。这样,你可以随意选择测试的组合来运行,并且确保每次测试时系统都是干净的状态。

撰写回答