Services Blog English

Le patron de conception superfixture avec pytest

| par jpic | python pytest

pytest est un excellent framework de test Python qui repart d’une page blanche au lieu de suivre le patron xUnit.

Au lieu de :

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())

On fait :

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


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

Quelques avantages méritent d’être notés :

  • la fixture n’est pas créée dans chaque test
  • on gagne aussi un niveau d’indentation, ce qui est plutôt cool !

Cependant, que faire avec du code complexe où l’on teste la colle entre plus d’une douzaine de fixtures ?

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

On se retrouve avec beaucoup de fixtures dans la signature. D’où l’invention du patron superfixture. C’est simplement une classe de fixture avec des propriétés mises en cache, qui seront créées pour la durée du test au moment où elles sont nécessaires. C’est en gros du pur Python, qui exploite l’excellent décorateur functools.cached_property :

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,
        )

Ensuite, vous pouvez l’utiliser ainsi dans vos 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

Vous pouvez même transmettre des fixtures venant de plugins, par exemple :

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

Mais si vous voulez simplement qu’un objet soit créé, il faut appeler la propriété. Dans ce cas, ajoutez peut-être un commentaire pour que cela ne ressemble pas à une erreur :

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

Voilà ! Travaillez intelligemment, pas durement ;)

Work smart, not hard

Ils nous font confiance

Contact

logo