pytest superfixture design pattern
| by jpic | python pytestpytest 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 ;)