1

Suppose I have a class something like this:

class Class:
    def __init__(self, data):
        self.tab1 = data.A
        self.tab2 = data.B

    # Other methods which try to change the state of object
    def add1(self, num):
        self.tab1 += num

    def mul2(self, num):
        self.tab2 *= num

Using pytest, I'm trying to test the state of object of Class (say obj) when several operations are done. For each operation I want a different test function but all of them should manipulate the same object. Since pytest doesn't allow using __init__ for test classes, only options I have is to create obj as class variable or return it from a class-scoped fixture as mentioned in this SO thread. But I can't make obj class variable because it needs class_data fixture for instantiating, so I return it from a class-scoped fixture, like this:

@pytest.fixture(scope='class')
def class_data():
    return pd.DataFrame({'A': [1,2,3], 'B': [4,5,6]})

class TestClass:
    @pytest.fixture(scope='class')
    def obj(self, class_data):
        return Class(class_data)

    def test_state1(self, obj):
        obj.add1(5)
        print(obj.tab1, obj.tab2)
        assert obj.tab1.equals(pd.Series([1, 2, 3])+5)

    def test_state2(self, obj):
        obj.mul2(2)
        print(obj.tab1, obj.tab2)
        assert obj.tab2.equals(pd.Series([4, 5, 6])*2)

So far so good, but what if I have so many methods - I don't want to pass it individually to each method. Thus following this SO answer, I tried to create a autouse fixture (with class scope), like this:

class TestClass:
    @pytest.fixture(autouse=True, scope='class')
    def setup_the_class(self, class_data):
        self.obj = Class(class_data)

    def test_state1(self):
        self.obj.add1(5)
        print(self.obj.tab1, self.obj.tab2)
        assert self.obj.tab1.equals(pd.Series([1, 2, 3])+5)

    def test_state2(self):
        self.obj.mul2(2)
        print(self.obj.tab1, self.obj.tab2)
        assert self.obj.tab2.equals(pd.Series([4, 5, 6])*2)

But it gives AttributeError: 'TestClass' object has no attribute 'obj'.

I don't understand why this happens? Even pytest docs mentions:

The class-level transact fixture is marked with autouse=True which implies that all test methods in the class will use this fixture without a need to state it in the test function signature or with a class-level usefixtures decorator.

Can anyone explain to me what is going wrong here and how autouse fixtures actually work? Is there a better way to do what I'm trying to achieve?

Besides, this autouse fixture approach also seems useful to me because it allows me (in theory) to define several other objects which use a fixture for their creation (like obj does) as members of TestClass (as self.<some_variable>) so that I will not need to define each of them in the separate class-scoped fixture that returns them. This theory is coming from my observation (assumption perhaps) that fixtures with autouse (or usefixtures) do not return/yield anything they just change the state of an entity that will be used by test methods, indirectly i.e. without passing as an argument. So my second question is whether I'm thinking in the right direction or not?

Jaladh Singhal
  • 393
  • 4
  • 10

1 Answers1

0

If you are using a class scoped fixture, the self parameter is not the same instance as the instance used in tests. The instance used in the fixture is created before all test instances, the fixture code until the yield is called, then for each test a new test instance is created, executed and deletd, and after that the code after the yield (e.g. the teardown code, if there is any) is called, and the fixture instance is deleted.

So, while you can't use self to store your object, you can store it in the class instead using self.__class__:

    @pytest.fixture(autouse=True, scope='class')
    def setup_the_class(self, class_data):
        self.__class__.obj = Class(class_data)

The data can be accessed from the tests as before as self.obj, because the lookup will find it in the class if it does not exist in the instance.

That being said, your test code does not look like you want to have a class scoped fixture. You change the value of the fixture in each test, and it stays changed that way, so te test outcome depends on the sequence the tests are executed. This is true for both versions of the test. In case this true in your real test, you probably want to use a function scoped fixture instead, that recreates the object for each test (and wehre you can save it in an instance variable).

MrBean Bremen
  • 14,916
  • 3
  • 26
  • 46