7

我目前有一个类似于这些的项目和测试。

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,这些测试(或类似的)通过了。对于更高版本的 py.test,test_conversion 和 test_a_works_with_b 失败,并显示“TestMyStuff 没有属性 a”。

我猜这是因为在 py.test 的后续版本中,会为每个测试的方法创建一个单独的 TestMyStuff 实例。

编写这些测试的正确方法是什么,以便可以为序列中的每个步骤给出结果,但可以(必须)使用先前(成功)测试的状态来执行后续测试?

4

5 回答 5

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 保证测试按文件顺序运行,测试功能可以更确切地说它们将在测试“状态”上增量工作。

但是,它有点脆弱,因为如果您通过例如“py.test -k test_conversion”选择仅运行“test_conversion”,那么您的测试将失败,因为第一个测试尚未运行。我认为进行增量测试的某种方法会很好,所以也许我们最终可以找到一个完全可靠的解决方案。

HTH,霍尔格

于 2010-07-15T08:55:45.610 回答
9

良好的单元测试实践是避免跨测试累积状态。大多数单元测试框架都竭尽全力阻止您累积状态。原因是您希望每个测试都独立存在。这使您可以运行测试的任意子集,并确保您的系统在每次测试时都处于干净状态。

于 2010-06-30T00:35:24.993 回答
7

这肯定是 pytest 固定装置的工作:https ://docs.pytest.org/en/latest/fixture.html

Fixtures 允许测试函数轻松接收和处理特定的预初始化应用程序对象,而无需关心导入/设置/清理细节。这是依赖注入的一个典型例子,其中夹具函数扮演注入器的角色,而测试函数是夹具对象的消费者。

因此,设置夹具以保持状态的示例如下:

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

请注意,您可以指定依赖注入的范围(以及状态)。在这种情况下,我已将其设置为会话,另一个选项是module(scope=function不适用于您的用例,因为您会丢失函数之间的状态。

显然,您可以扩展此模式以在状态中保存其他类型的对象,例如比较不同测试的结果。

作为警告 - 您仍然希望能够以任何顺序运行测试(我的示例违反了 this 交换 1 和 2 的顺序导致失败)。但是,为了简单起见,我没有说明这一点。

于 2017-07-11T14:42:58.557 回答
2

为了补充hpk42 的答案,您还可以使用pytest-steps来执行增量测试,如果您希望在步骤之间共享某种增量状态/中间结果,这尤其可以帮助您。

使用这个包,您不需要将所有步骤放在一个类中(可以,但不是必需的),只需用@test_steps.

编辑:有一个新的“生成器”模式使它更容易:

from pytest_steps import test_steps

@test_steps('step_first', 'step_conversion', 'step_a_works_with_b')
def test_suite_with_shared_results():
    a = mylib.get_a()
    yield

    b = mylib.convert_a_to_b(a)
    yield

    assert mylib.works_with(a, b)
    yield

旧版答案:

steps_data如果您希望StepsDataHolder在您的步骤之间共享一个对象,您可以向您的测试函数添加一个参数。

然后,您的示例将编写:

from pytest_steps import test_steps, StepsDataHolder

def step_first(steps_data):
    steps_data.a = mylib.get_a()


def step_conversion(steps_data):
    steps_data.b = mylib.convert_a_to_b(steps_data.a)


def step_a_works_with_b(steps_data):
    assert mylib.works_with(steps_data.a, steps_data.b)


@test_steps(step_first, step_conversion, step_a_works_with_b)
def test_suite_with_shared_results(test_step, steps_data: StepsDataHolder):

    # Execute the step with access to the steps_data holder
    test_step(steps_data)

最后,请注意,如果另一个步​​骤使用失败,您可以自动跳过或失败@depends_on,请查看文档以获取详细信息。

(顺便说一句,我是这个包的作者;))

于 2018-07-27T16:10:17.660 回答
1

随着我在这个问题上花费更多时间,我意识到我的问题有一个隐含的方面,我忽略了具体说明。在大多数情况下,我发现我想在单个类中累积状态,但在测试类完成时将其丢弃。

我最终为我的一些类使用了什么,类本身代表一个累积状态的过程,我将累积的状态存储在类对象本身中。

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.__class__.a = mylib.get_a()

    def test_conversion(self):
        self.__class__.b = mylib.convert_a_to_b(self.a)

    def test_a_works_with_b(self):
        mylib.works_with(self.a, self.b)

这种方法的优点是它将状态封装在测试类中(没有必须存在的辅助函数才能运行测试),并且对于不同的类来说期望 TestMyStuff 状态是适当的尴尬在不同的班级运行时出现。

我认为到目前为止讨论的每种方法都有其优点,并打算在最适合的地方使用每种方法。

于 2010-07-15T13:04:28.487 回答