简体   繁体   中英

Pytest - combining multiple fixture parameters into a single fixture to optimise fixture instantiation

I have an existing pytest test that makes use of some predefined lists to test the cross-product of them all:

A_ITEMS = [1, 2, 3]
B_ITEMS = [4, 5, 6]
C_ITEMS = [7, 8, 9]

I also have an expensive fixture that has internal conditions dependent on A and B items (but not C), called F:

class Expensive:
    def __init__(self):
        # expensive set up
        time.sleep(10)
    def check(self, a, b, c):
        return True  # keep it simple, but in reality this depends on a, b and c

@pytest.fixture
def F():
    return Expensive()

Currently I have a naive approach that simply parametrizes a test function:

@pytest.mark.parametrize("A", A_ITEMS)
@pytest.mark.parametrize("B", B_ITEMS)
@pytest.mark.parametrize("C", C_ITEMS)
def test_each(F, A, B, C):
    assert F.check(A, B, C)

This tests all combinations of F with A, B and C items, however it constructs a new Expensive instance via the F fixture for every test. More specifically, it reconstructs a new Expensive via fixture F for every combination of A, B and C.

This is very inefficient, because I should only need to construct a new Expensive when the values of A and B change, which they don't between all tests of C.

What I would like to do is somehow combine the F fixture with the A_ITEMS and B_ITEMS lists, so that the F fixture only instantiates a new instance once for each run through the values of C.

My first approach involves separating the A and B lists into their own fixtures and combining them with the F fixture:

class Expensive:
    def __init__(self, A, B):
        # expensive set up
        self.A = A
        self.B = B
        time.sleep(10)
    def check(self, c):
        return True  # keep it simple

@pytest.fixture(params=[1,2,3])
def A(request):
    return request.param

@pytest.fixture(params=[4,5,6])
def B(request):
    return request.param

@pytest.fixture
def F(A, B):
    return Expensive(a, b)

@pytest.mark.parametrize("C", C_ITEMS)
def test_each2(F, C):
    assert F.check(C)

Although this tests all combinations, unfortunately this creates a new instance of Expensive for each test, rather than combining each A and B item into a single instance that can be reused for each value of C.

I've looked into indirect fixtures, but I can't see a way to send multiple lists (ie both the A and B items) to a single fixture.

Is there a better approach I can take with pytest? Essentially what I'm looking to do is minimise the number of times Expensive is instantiated, given that it's dependent on values of item A and B.

Note: I've tried to simplify this, however the real-life situation is that F represents creation of a new process, A and B are command-line parameters for this process, and C is simply a value passed to the process via a socket. Therefore I want to be able to send each value of C to this process without recreating it every time C changes, but obviously if A or B change, I need to restart it (as they are command-line parameters to the process).

If using pytest scoping (as proposed in the other answer) is not an option, you may try to cache the expansive object, so that it will be constructed only if needed.

Basically, this expands the proposal given in the question with an additional static caching of the last used parameters to avoid creating a new Expansive if not needed:

@pytest.fixture(params=A_ITEMS)
def A(request):
    return request.param


@pytest.fixture(params=B_ITEMS)
def B(request):
    return request.param


class FFactory:
    lastAB = None
    lastF = None

    @classmethod
    def F(cls, A, B):
        if (A, B) != cls.lastAB:
            cls.lastAB = (A, B)
            cls.lastF = Expensive(A, B)
        return cls.lastF

@pytest.fixture
def F(A, B):
    return FFactory.F(A, B)


@pytest.mark.parametrize("C", C_ITEMS)
def test_each(F, C):
    assert F.check(C)

I've had some success using a more broadly scoped fixture (module or session) as "cache" for the per-test fixture for this sort of situation where the "lifetimes" of the fixtures proper don't align cleanly with amortised costs or whatever.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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