twil
twil

Reputation: 103

pytest: How to use test class instance variables as fixture params

I am trying to reuse some unit testing functionality. The only variable part is actually the set of test data. Therefore I encapsulate the testing logic in a class and inject the actual test data at object creation.

Unfortunately this does not seem to work with the @pytest.fixture decorator.

Some minimal code to reproduce. This is the common definition of my test class in define_test.py:

#!/usr/bin/env python3

import pytest

class TestDemo:
    def __init__(self, _data: list):
        self.__testData = _data

    @pytest.fixture(scope='module', params=self.__testData)
    def fixture(self, request):
        return request.param

    def test(self, fixture):
        foo = fixture
        assert foo is not None

Intended usage would be something like this in use_test.py

#!/usr/bin/env python3

import define_test

data1 = [1, 2, 3, 4]
test1 = define_test.TestDemo(data1)
data2 = [4, 3, 2, 1]
test2 = define_test.TestDemo(data2)

Trying to run this code produces

define_test.py:9: in TestDemo
    ???
E   NameError: name 'self' is not defined

I am pretty sure that the problem is, that one cannot simply apply the usual OO mechanisms to the pytest decorator. It seems as either the decorator is evaluated before even an instance of this class exists or it cannot handle instances at all.

So what would be the correct way of achieving the goal of reusable tests with variable data?

Upvotes: 1

Views: 1956

Answers (1)

cms
cms

Reputation: 5992

You might be able to express this by dynamic parameterization which you can access through the hook pytest_generate_tests

there are some examples in the docs

This way you can intercept the tests at collection time and inject your own parameterize settings. The hook is called for each test with a metafunc object as the argument which exposes a parameterize method

The metafunc documentation is here

It doesn't look to me like you will be able to access instance data, because this code is happening at collect time, before your objects are initialised. But you can access the class object via metafunc.cls, so maybe you could have your methods in a superclass, and then subclass this to add specific data cases as class attributes

here is some example code of the kind of thing I am thinking about

a) pytestfoo.py

#!/usr/bin/env python3

import pytest


class BaseTest:

    def test(self, testparams):
        print("\n", testparams, "\n")
        assert testparams is not None

class TestCaseA(BaseTest):
    testparams = [[1,2,3,4]]

class TestCaseB(BaseTest):
    testparams = [[4,3,2,1]]

b) the corresponding conftest.py

def pytest_generate_tests(metafunc):
    metafunc.parametrize("testparams", metafunc.cls.testparams)

and a run of these tests using pytest


pytest -sv pytestfoo.py
========================================================== test session starts ==========================================================
platform linux -- Python 3.7.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- /home/cms/.pyenv/versions/3.7.6/envs/tmp/bin/python3.7
cachedir: .pytest_cache
rootdir: /home/cms/tmp
collected 2 items                                                                                                                       

pytestfoo.py::TestCaseA::test[testparams0] 
 [1, 2, 3, 4] 

PASSED
pytestfoo.py::TestCaseB::test[testparams0] 
 [4, 3, 2, 1] 

PASSED

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

hopefully there is some inspiration here for your own experiments. The pytest docs are showing how to paramaterize with data from the command line, which might be more practically useful than the approach above, but I was trying to follow your lead in defining the data with code.

Upvotes: 2

Related Questions