Christoph
Christoph

Reputation: 2940

pytest parametrised fixture with different cases

I have several test cases and test functions, and the list of test cases is different for the different functions. This can be easily done with pytest.mark.parametrize. The extra need I have is to load a resource (a file in my case) and I'd like to have this file only loaded once per test session and cached.

Below an example illustrating what I want. It's working, but I would like to find a way to use pytest fixtures or some other caching mechanism so that I don't have to do the caching myself and have the pars=load_file(pars) line in each test function.

Can someone please explain how to do this with pytest?

import pytest

case_1 = dict(label='case_1', spam=1)
case_2 = dict(label='case_2', spam=2)
case_3 = dict(label='case_3', spam=3)

_cache = {}


def load_file(pars):
    if pars['label'] in _cache:
        print('load_file from cache', pars)
        return _cache[pars['label']]
    else:
        print('load_file loading', pars)
        pars['file'] = pars['label'] + ' spam!'
        _cache[pars['label']] = pars
        return pars


@pytest.mark.parametrize('pars', [case_1, case_2])
def test_a(pars):
    pars = load_file(pars)
    print('test_a', pars)


@pytest.mark.parametrize('pars', [case_2, case_3])
def test_b(pars):
    pars = load_file(pars)
    print('test_b', pars)


@pytest.mark.parametrize('pars', [case1, case_2, case_3])
def test_c(pars):
    pars = load_file(pars)
    print('test_c', pars)

### more tests here for various combinations of test cases

Upvotes: 3

Views: 1128

Answers (2)

smarie
smarie

Reputation: 5156

A simple use of @lru_cache in your file parsing function can also do the caching trick:

@lru_cache(maxsize=3)
def load_file(file_name):
    """ This function loads the file and returns contents"""
    print("loading file " + file_name)
    return "<dummy content for " + file_name + ">"

You can also reach the same result while making the whole code a bit more readable by separating the test functions from the test cases with pytest-cases (I'm the author by the way!):

from functools import lru_cache
from pytest_cases import parametrize_with_cases

@lru_cache(maxsize=3)
def load_file(file_name):
    """ This function loads the file and returns contents"""
    print("loading file " + file_name)
    return "<dummy content for " + file_name + ">"

def case_1():
    return load_file('file1')

def case_2():
    return load_file('file2')

def case_3():
    return load_file('file3')

@parametrize_with_cases("pars", cases=[case_1, case_2])
def test_a(pars):
    print('test_a', pars)

@parametrize_with_cases("pars", cases=[case_2, case_3])
def test_b(pars):
    print('test_b', pars)

@parametrize_with_cases("pars", cases=[case_1, case_2, case_3])
def test_c(pars):
    print('test_c', pars)

Yields:

loading file file1
test_a <dummy content for file1>PASSED
loading file file2
test_a <dummy content for file2>PASSED
test_b <dummy content for file2>PASSED
loading file file3
test_b <dummy content for file3>PASSED
test_c <dummy content for file1>PASSED
test_c <dummy content for file2>PASSED
test_c <dummy content for file3>PASSED

Finally note that depending on your use case you might wish to switch to a case generator by using @parametrize on a case function, that could be more readable:

from pytest_cases import parametrize

@parametrize("file_name", ["file1", "file2"])
def case_gen(file_name):
    return load_file(file_name)

Also look at tags & filters, if you do not want to hardcode the cases explicitly.

Upvotes: 1

Sergey Vasilyev
Sergey Vasilyev

Reputation: 4189

The first and the obvious solution is to use the session-scoped fixtures. However, it requires restructuring the test file, and load all of the known files in advance.

import pytest

@pytest.fixture(scope='session')
def pars_all():
   cache = {}
   for case in [case_1, case_2, case_3]:
       cache[case['label']] = 'case {} content'.format(case)

   yield cache

   # optionally destroy or unload or unlock here.

@pytest.fixture(scope='function')
def pars(request, pars_all):
    label = request.param
    yield pars_all[label]

@pytest.mark.parametrize('pars', ['case_1', 'case_2'], indirect=True)
def test(pars):
    pass

Please note the indirect parametrisation. It means that the pars fixture will be prepared instead, getting a parameter value in request.param. The parameter name and the fixture must share the same name.

The session-scoped fixture (or module-scoped, or class-scoped if you wish) will be prepared only once for all the tests. It is important to note that the wider-scoped fixtures can be used in the more narrow-scoped or same-scoped fixtures, but not in the opposite direction.

If the cases are not that well-defined, it is the same easy, just the cache is populated on demand:

import pytest

@pytest.fixture(scope='session')
def pars_all():
    yield {}

@pytest.fixture(scope='function')
def pars(request, pars_all):
    label = request.param
    if label not in pars_all:
        print('[[[{}]]]'.format(request.param))
        pars_all[label] = 'content of {}'.format(label)
    yield pars_all[label]

@pytest.mark.parametrize('pars', ['case_1', 'case_2'], indirect=True)
def test_1(pars):
    print(pars)

@pytest.mark.parametrize('pars', ['case_1', 'case_3'], indirect=True)
def test_2(pars):
    print(pars)

Note, that the {} object is created only once, because it is session-scoped, and is shared among all tests & callspecs. So, if one fixture adds something into it, other fixtures will see it too. You can notice that on how case_1 is reused in the test_2:

$ pytest -s -v -ra test_me.py 
======= test session starts ==========
...
collected 4 items                                                                                   

test_me.py::test_1[case_1] [[[case_1]]]
content of case_1
PASSED
test_me.py::test_1[case_2] [[[case_2]]]
content of case_2
PASSED
test_me.py::test_2[case_1] content of case_1
PASSED
test_me.py::test_2[case_3] [[[case_3]]]
content of case_3
PASSED

======== 4 passed in 0.01 seconds ==========

Upvotes: 1

Related Questions