Services Blog Français

pytest superfixture design pattern

| by jpic | python pytest

pytest is an awsome Python Test framework which starts over from a clean slate instead of following the xUnit pattern.

Instead of:

class YourTestSuite(unittest.TestCase):
    def setUp(self):
        self.some_stuff = YourThing()

    def test_delete(self):
        self.some_stuff.delete()
        self.assertFalse(self.some_stuff.exists())

We do:

@pytest.fixture
def some_stuff():
    return YourThing()


def test_delete(some_stuff):
    some_stuff.delete()
    assert not some_stuff.exists()

A couple of advantages are worth noting:

  • the fixture is not created in every test
  • we also win one indentation level which is pretty cool!

However, what about complex code where we test the glue between over a dozen of fixtures?

def test_delete(stuff, client, timestamp, bottle_contract, redeem_contract, blockchain, account):
    # ...

We end up with a lot of fixtures in the signature. Hence the invention of the superfixture pattern. It’s just a fixture class with cached properties that will be created for the time of the test as needed, it’s basically pure python, leveraging the awesome functools.cached_property decorator:

class Fixture:
    @functools.cached_property
    def stuff(self):
        return Stuff()

    @functools.cached_property
    def blockchain(self):
        return Blockchain.objects.create(name='ethlocal')

    @functools.cached_property
    def account(self):
        return self.blockchain.account_set.create()

    @functools.cached_property
    def client(self):
        client = APIClient()
        client.login(self.account)
        return client

    @functools.cached_property
    def bottle_contract(self):
        return BottleContract.objects.create(
            owner=self.account,
            blockchain=self.blockchain,
        )

    @functools.cached_property
    def redeem_contract(self):
        return RedeemContract.objects.create(
            bottle_contract=self.bottle_contract,
            owner=self.account,
            blockchain=self.blockchain,
        )

Then, you can use it as such in your tests:

@pytest.fixture
def fixture():
    return Fixture()


def test_something(fixture):
    response = fixture.client.post(
        '/redeem',
        dict(contract=fixture.redeem_contract),
    )
    fixture.redeem_contract.refresh_from_db()
    assert fixture.redeem_contract.redeemed

You can even pass through fixtures from plugins, ie:

@pytest.fixture
def fixture(mocker):
    return Fixture(mocker)

But, if you just want an object to be created, you need to call the property, in this case, maybe add a comment so that it doesn’t look like a mistake:

def test_something(fixture):
    fixture.redeem_contract  # ensure redeem contract creation prior to test
    # stuff

That’s it! Work smart, not hard ;)

Work smart, not hard

They trust us

Contact

logo