2

I created a class to make my life easier while doing some integration tests involving workers and their contracts. The code looks like this:

class ContractID(str):
    contract_counter = 0
    contract_list = list()

    def __new__(cls):
        cls.contract_counter += 1
        new_entry = super().__new__(cls, f'Some_internal_name-{cls.contract_counter:10d}')
        cls.contract_list.append(new_entry)
        return new_entry

    @classmethod
    def get_contract_no(cls, worker_number):
        return cls.contract_list[worker_number-1]  # -1 so WORKER1 has contract #1 and not #0 etc.

When I'm unit-testing the class, I'm using the following code:

from test_helpers import ContractID

@pytest.fixture
def get_contract_numbers():
    test_string_1 = ContractID()
    test_string_2 = ContractID()
    test_string_3 = ContractID()
    return test_string_1, test_string_2, test_string_3


def test_contract_id(get_contract_numbers):
    assert get_contract_ids[0] == 'Some_internal_name-0000000001'
    assert get_contract_ids[1] == 'Some_internal_name-0000000002'
    assert get_contract_ids[2] == 'Some_internal_name-0000000003'


def test_contract_id_get_contract_no(get_contract_numbers):
    assert ContractID.get_contract_no(1) == 'Some_internal_name-0000000001'
    assert ContractID.get_contract_no(2) == 'Some_internal_name-0000000002'
    assert ContractID.get_contract_no(3) == 'Some_internal_name-0000000003'
    with pytest.raises(IndexError) as py_e:
        ContractID.get_contract_no(4)
    assert py_e.type == IndexError

However, when I try to run these tests, the second one (test_contract_id_get_contract_no) fails, because it does not raise the error as there are more than three values. Furthermore, when I try to run all my tests in my folder test/, it fails even the first test (test_contract_id), which is probably because I'm trying to use this function in other tests that run before this test.

After reading this book, my understanding of fixtures was that it provides objects as if they were never called before, which is obviously not the case here. Is there a way how to tell the tests to use the class as if it hasn't been used before anywhere else?

0

1 Answer 1

1

If I understand that correctly, you want to run the fixture as setup code, so that your class has exactly 3 instances. If the fixture is function-scoped (the default) it is indeed run before each test, which will each time create 3 new instances for your class. If you want to reset your class after the test, you have to do this yourself - there is no way pytest can guess what you want to do here.

So, a working solution would be something like this:

@pytest.fixture(autouse=True)
def get_contract_numbers():
    test_string_1 = ContractID()
    test_string_2 = ContractID()
    test_string_3 = ContractID()
    yield 
    ContractID.contract_counter = 0
    ContractID.contract_list.clear()

def test_contract_id():
    ...

Note that I did not yield the test strings, as you don't need them in the shown tests - if you need them, you can yield them, of course. I also added autouse=True, which makes sense if you need this for all tests, so you don't have to reference the fixture in each test.

Another possibility would be to use a session-scoped fixture. In this case the setup would be done only once. If that is what you need, you can use this instead:

@pytest.fixture(autouse=True, scope="session")
def get_contract_numbers():
    test_string_1 = ContractID()
    test_string_2 = ContractID()
    test_string_3 = ContractID()
    yield 
Sign up to request clarification or add additional context in comments.

2 Comments

I thought maybe there was something, that would allow each test to start with a clean slate, like scope="test", which would forget about everything from other tests and run the test as if no tests (or anything really) were run before. In the end, I decided to go with your solution of yielding and clearing everything after. However, my note for this is that this could prove difficult for some more complex classes.
I understand your sentiment, but think about how to implement such a clean slate - there is no way to know what to do other than completely clear all Python objects, which is not possible if you want to retain some state for running the tests. You can run the tests in separate processes, and even that would not ensure that no side effects like changing files could happen between the tests. Running each tests in a new docker container would do that, but if you don't want that overhead you have to make sure that you clean up the changed state yourself.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.