简体   繁体   English

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

[英]How to accumulate state across tests in py.test

I currently have a project and tests similar to these. 我目前有一个类似于这些的项目和测试。

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)

With py.test 0.9.2, these tests (or similar ones) pass. 使用py.test 0.9.2,这些测试(或类似测试)通过。 With later versions of py.test, test_conversion and test_a_works_with_b fail with 'TestMyStuff has no attribute a'. 对于更高版本的py.test,test_conversion和test_a_works_with_b失败并且'TestMyStuff没有属性a'。

I am guessing this is because with later builds of py.test, a separate instance of TestMyStuff is created for each method that is tested. 我猜这是因为在py.test的后续版本中,为每个被测试的方法创建了一个单独的TestMyStuff实例。

What is the proper way to write these tests such that results can be given for each of the steps in the sequence, but the state from a previous (successful) test can (must) be used to perform subsequent tests? 编写这些测试的正确方法是什么,以便可以为序列中的每个步骤提供结果,但是可以(必须)使用先前(成功)测试的状态来执行后续测试?

Good unit test practice is to avoid state accumulated across tests. 良好的单元测试实践是避免测试中累积的状态。 Most unit test frameworks go to great lengths to prevent you from accumulating state. 大多数单元测试框架都会竭尽全力阻止您积累状态。 The reason is that you want each test to stand on its own. 原因是你希望每个测试都独立存在。 This lets you run arbitrary subsets of your tests, and ensures that your system is in a clean state for each test. 这使您可以运行测试的任意子集,并确保系统处于每个测试的干净状态。

I partly agree with Ned in that it's good to avoid somewhat random sharing of test state. 我部分同意Ned的观点,即避免在某种程度上随机分享测试状态是件好事。 But i also think it is sometimes useful to accumulate state incrementally during tests. 但我也认为在测试期间逐步累积状态有时是有用的。

With py.test you can actually do that by making it explicit that you want to share test state. 使用py.test,您可以通过明确表示您想要共享测试状态来实现这一点。 Your example rewritten to work: 您的示例重写为工作:

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)

You can run this with recent py.test versions. 您可以使用最近的py.test版本运行它。 Each functions receives a "state" object and the "funcarg" factory creates it initially and caches it over the module scope. 每个函数都接收一个“状态”对象,“funcarg”工厂最初创建它并将其缓存在模块范围内。 Together with the py.test guarantee that tests are run in file order the test functions can be rather they will work incrementally on the test "state". 与py.test一起保证测试按文件顺序运行,测试函数可以相反,它们将在测试“状态”上递增地工作。

However, It is a bit fragile because if you select just the running of "test_conversion" via eg "py.test -k test_conversion" then your test will fail because the first test hasn't run. 但是,它有点脆弱,因为如果您通过例如“py.test -k test_conversion”选择仅运行“test_conversion”,那么您的测试将失败,因为第一个测试尚未运行。 I think that some way to do incremental tests would be nice so maybe we can eventually find a totally robust solution. 我认为进行增量测试的一些方法会很好,所以也许我们最终可以找到一个完全可靠的解决方案。

HTH, holger HTH,holger

This is surely a job for pytest fixtures: https://docs.pytest.org/en/latest/fixture.html 这肯定是pytest装置的工作: https ://docs.pytest.org/en/latest/fixture.html

Fixtures allow test functions to easily receive and work against specific pre-initialized application objects without having to care about import/setup/cleanup details. Fixtures允许测试功能轻松接收和处理特定的预先初始化的应用程序对象,而无需关心导入/设置/清理细节。 It's a prime example of dependency injection where fixture functions take the role of the injector and test functions are the consumers of fixture objects. 这是依赖注入的一个主要示例,其中夹具功能扮演注入器的角色,测试功能是夹具对象的消费者。

So an example of setting up a fixture to hold state would be as follows: 因此,设置夹具以保持状态的示例如下:

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

Note that you can specify the scope for the dependency injection (and hence the state). 请注意,您可以指定依赖项注入的范围(以及状态)。 In this case I have set it to session, the other option would be module ( scope=function wouldn't work for your use-case as you would lose state between functions. 在这种情况下,我将其设置为session,另一个选项是modulescope=function不适用于您的用例,因为您将失去函数之间的状态。

Obviously you can extend this pattern to hold other types of objects in the state, such as comparing outcomes from different tests. 显然,您可以扩展此模式以保存状态中的其他类型的对象,例如比较来自不同测试的结果。

As a word of warning - you still want to be able to run your tests in any order (my example breaches this s swapping the order of 1 and 2 results in failure). 作为一个警告 - 你仍然希望能够以任何顺序运行你的测试(我的例子违反了这个交换1和2的顺序导致失败)。 However I have not illustrated that for the sake of simplicity. 但是为了简单起见,我没有说明这一点。

To complement hpk42's answer , you can also use pytest-steps to perform incremental testing, this can help you in particular if you wish to share some kind of incremental state/intermediate results between the steps. 为了补充hpk42的答案 ,您还可以使用pytest-steps来执行增量测试,如果您希望在步骤之间共享某种增量状态/中间结果,这可以帮助您。

With this package you do not need to put all the steps in a class (you can, but it is not required), simply decorate your "test suite" function with @test_steps . 使用这个包你不需要把所有的步骤放在一个类中(你可以,但不是必需的),只需用@test_steps装饰你的“测试套件”功能。

EDIT: there is a new 'generator' mode to make it even easier: 编辑:有一个新的'发电机'模式,使它更容易:

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

LEGACY answer: LEGACY回答:

You can add a steps_data parameter to your test function if you wish to share a StepsDataHolder object between your steps. 如果要在步骤之间共享StepsDataHolder对象,可以向测试函数添加steps_data参数。

Your example would then write: 然后你的例子会写:

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)

Finally, note that you can automatically skip or fail a step if another has failed using @depends_on , check in the documentation for details. 最后,请注意,如果其他人使用@depends_on失败,您可以自动跳过或失败一个步骤,请查看文档以获取详细信息。

(I'm the author of this package by the way ;) ) (顺便说一下,我是这个包的作者;))

As I spent more time with this problem, I realized there was an implicit aspect to my question that I neglected to specify. 当我花更多时间研究这个问题时,我意识到我的问题有一个隐含的方面,我忽略了。 In most scenarios, I found that I wanted to accumulate state within a single class, but discard it when the test class had completed. 在大多数情况下,我发现我想在一个类中累积状态,但在测试类完成时丢弃它。

What I ended up using for some of my classes, where the class itself represented a process that accumulated state, I stored the accumulated state in the class object itself. 我最终用于某些类,其中类本身表示一个累积状态的进程,我将累积状态存储在类对象本身中。

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)

The advantage to this approach is it keeps the state encapsulated within the test class (there are no auxiliary functions that have to be present for the test to run), and it would be suitably awkward for a different class to expect the TestMyStuff state to be present when the different class runs. 这种方法的优点是它保持封装在测试类中的状态(没有必须存在的辅助函数来运行测试),并且对于不同的类来说期望TestMyStuff状态是合适的尴尬当不同的班级运行时出现。

I think each of these approaches discussed thusfar have their merits, and intend to use each approach where it fits best. 我认为这样讨论的每种方法都有它们的优点,并且打算在最适合的地方使用每种方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM