Pytest: parametrizing tests that require a slow initialization

Viewed 482

I want to do tests with randomized parameters of a class with a very slow init method. The tests themself are very quick, but require a time consuming initialization step. Of course. I do something like this:

@pytest.mark.parametrize("params", LIST_OF_RANDOMIZED_PARAMS)
def test_one(params):
    state = very_slow_initialization(params)
    assert state.fast_test()

@pytest.mark.parametrize("params", LIST_OF_RANDOMIZED_PARAMS)
def test_two(params):
    state = very_slow_initialization(params)
    assert state.another_fast_test()

From my unsuccessful tries so far I've learnt:

  1. initializing a Testclass with a parametrized set_class(params) method is not supported
  2. Using a fixture that initialized the class still calls the slow initialization every time
  3. I could create a list with all initialized states in advance, however they demand a lot of memory. Furthermore sometimes I like to rune a lot of randomized tests overnight and just stop them the next morning. This this I would need to know precisely how many tests I should to so that all initializations are finished before that.
  4. If possible I would prefer a solution that runs both tests for the first parameter, then runs both with the second parameter and so on.

There is probably a really simple solution for this.

2 Answers

pytest fixtures is a solution for you. Lifetime of fixture might be a single test, class, module or whole test session.

fixture management scales from simple unit to complex functional testing, allowing to parametrize fixtures and tests according to configuration and component options, or to re-use fixtures across function, class, module or whole test session scopes.

Per Fixture availability paragraph, you need to define feature in class, or on module level.

Consider using module-scoped ones (pay attention, that initialization launched only once):

import pytest


@pytest.fixture(scope="module")
def heavy_context():
    # Use your LIST_OF_RANDOMIZED_PARAMS randomized parameters here
    # to initialize whatever you want.
    print("Slow fixture initialized")
    return ["I'm heavy"]


def test_1(heavy_context):
    print(f"\nUse of heavy context: {heavy_context[0]}")


def test_2(heavy_context):
    print(f"\nUse of heavy context: {heavy_context[0]}")

Tests output:

...
collecting ... collected 2 items

test_basic.py::test_1 Slow fixture initialized
PASSED                                             [ 50%]
Use of heavy context: I'm heavy

test_basic.py::test_2 PASSED                                             [100%]
Use of heavy context: I'm heavy

Now, if you need it to be assertion safe (release resources even when test fails), consider creating heavy_context in a context-manager manner (much more details here: Fixture, Running multiple assert statements safely):

import pytest


@pytest.fixture(scope="module")
def heavy_context():

    print("Slow context initialized")
    obj = ["I'm heavy"]

    # It is mandatory to put deinitialiation into "finally" scope
    # otherwise in case of exception it won't be executed
    try:
        yield obj[0]
    finally:
        print("Slow context released")


def test_1(heavy_context):
    # Pay attention, that in fact heavy_context now
    # is what we initialized as 'obj' in heavy_context
    # function.
    print(f"\nUse of heavy context: {heavy_context}")


def test_2(heavy_context):
    print(f"\nUse of heavy context: {heavy_context}")

Output:

collecting ... collected 2 items

test_basic.py::test_1 Slow context initialized
PASSED                                             [ 50%]
Use of heavy context: I'm heavy

test_basic.py::test_2 PASSED                                             [100%]
Use of heavy context: I'm heavy
Slow context released


============================== 2 passed in 0.01s ===============================

Process finished with exit code 0

Could you perhaps run the tests one after another without initializing the object again, e.g.:

@pytest.mark.parametrize("params", LIST_OF_RANDOMIZED_PARAMS)
def test_one(params):
    state = very_slow_initialization(params)
    assert state.fast_test()
    assert state.another_fast_test()

or using separate functions for organization:

@pytest.mark.parametrize("params", LIST_OF_RANDOMIZED_PARAMS)
def test_main(params):
    state = very_slow_initialization(params)

    step_one(state)
    step_two(state)

def step_one(state):
    assert state.fast_test()

def step_two(state):
    assert state.another_fast_test()

Although it's a test script, you can still use functions to organize your code. In the version with separate functions you may even declare a fixture, in case the state may be needed in other tests, too:

@pytest.fixture(scope="module", params=LIST_OF_RANDOMIZED_PARAMS)
def state(request):
    return very_slow_initialization(request.param)

def test_main(state):
    step_one(state)
    step_two(state)

def step_one(state):
    assert state.fast_test()

def step_two(state):
    assert state.another_fast_test()

I hope I didn't do a mistake here, but it should work like this.

Related