Tester les modèles Django avec factory_boy et pytest
Les usines (factories) factory_boy simplifient la génération d’instances liées de modèles Django dans les tests. Plutôt que de créer manuellement des dizaines de modèles avec des relations ForeignKey et ManyToMany, les usines s’appuient sur Faker pour générer automatiquement des données réalistes et variées. Cela accélère l’écriture des tests, élimine la duplication de code redondant et limite les ruptures lors de l’évolution des modèles.
Prenons un domaine bibliothécaire : Genre, Langue, Auteur et Livre (avec leurs relations).
class Genre(models.Model):
name = models.CharField(max_length=200)
class Langue(models.Model):
name = models.CharField(max_length=20)
class Auteur(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
date_de_naissance = models.DateField(null=True, blank=True)
date_de_deces = models.DateField(null=True, blank=True)
class Livre(models.Model):
title = models.CharField(max_length=200)
genre = models.ForeignKey('Genre', on_delete=models.CASCADE, null=True)
langue = models.ForeignKey('Langue', on_delete=models.CASCADE, null=True)
auteur = models.ManyToManyField('Auteur')
resume = models.TextField(max_length=1000)
isbn = models.CharField(max_length=13)
L’anti-patern : création manuelle d’objets
Instancier manuellement des objets liés introduit de sérieux problèmes de maintenabilité :
- Duplication systématique de code boilerplate dans chaque test
- Valeurs codées en dur au lieu d’une génération dynamique et réaliste de données
- Mélange flou entre la préparation des données de test et la logique des assertions
- Tests fragiles qui échouent dès la moindre modification mineure du modèle
Exemple de code problématique :
def test_creation_livre():
genre = Genre.objects.create(name="Science-fiction")
langue = Langue.objects.create(name="Russe")
auteur = Auteur.objects.create(
first_name="Arkadi",
last_name="Strougatski",
date_de_naissance="1925-08-28"
)
livre = Livre.objects.create(
title="Le Pique-nique au bord du chemin",
genre=genre,
langue=langue,
resume="L’une des nouvelles les plus acclamées…",
isbn="9785171180975"
)
livre.auteur.add(auteur)
assert livre.display_auteur() == "Strougatski"
Cette approche ne passe pas à l’échelle : elle devient ingérable et sujette aux erreurs dans des scénarios complexes impliquant des dizaines d’objets interconnectés.
Usines basiques avec Faker
factory_boy s’intègre parfaitement à Django et Faker pour générer des données de test cohérentes et réalistes :
class AuteurFactory(factory.django.DjangoModelFactory):
class Meta:
model = Auteur
first_name = factory.Faker("first_name")
last_name = factory.Faker("last_name")
date_de_naissance = factory.Faker("date_time")
date_de_deces = factory.Faker("date_time")
Créer un auteur se fait désormais en une seule ligne propre :
auteur = AuteurFactory.create()
Le même principe s’applique aux autres modèles :
class GenreFactory(factory.django.DjangoModelFactory):
class Meta:
model = Genre
name = factory.Faker("name")
class LangueFactory(factory.django.DjangoModelFactory):
class Meta:
model = Langue
name = factory.Faker("name")
Gérer les modèles liés
Pour les champs ForeignKey, utilisez SubFactory :
class LivreFactory(factory.django.DjangoModelFactory):
class Meta:
model = Livre
title = factory.Faker("sentence")
genre = factory.SubFactory(GenreFactory)
langue = factory.SubFactory(LangueFactory)
resume = factory.Faker("paragraph")
isbn = str(random.randint(1000000000000, 9999999999999))
Pour les champs ManyToManyField, utilisez @factory.post_generation :
@factory.post_generation
def auteur(self, create, extracted, **kwargs):
if not create or not extracted:
return
self.auteur.add(*extracted)
Un test clair et lisible :
def test_creation_livre():
auteur1 = AuteurFactory.create()
auteur2 = AuteurFactory.create()
livre = LivreFactory.create(auteur=[auteur1, auteur2])
assert len(livre.auteur.all()) == 2
Intégration avec pytest via des fixtures
Dans conftest.py, définissez une fixture réutilisable :
@pytest.fixture(scope='function')
def test_livre_factory() -> Livre:
auteur1: Auteur = AuteurFactory.create()
auteur2: Auteur = AuteurFactory.create()
livre_creer: Livre = LivreFactory.create(auteur=[auteur1, auteur2])
return livre_creer
Puis dans test_models.py :
@pytest.mark.django_db
def test_modele_livre(test_livre_factory: Livre) -> None:
titre: str = test_livre_factory.title
genre: Genre = test_livre_factory.genre
langue: Langue = test_livre_factory.langue
resume: str = test_livre_factory.resume
isbn: str = test_livre_factory.isbn
auteurs: QuerySet = test_livre_factory.auteur.all()
assert len(resume) <= 1000
assert len(titre) <= 200
assert len(isbn) <= 13
assert isinstance(titre, str)
assert isinstance(genre, Genre)
assert isinstance(langue, Langue)
assert isinstance(resume, str)
assert isinstance(isbn, str)
for auteur in auteurs:
assert isinstance(auteur, Auteur)
Le décorateur @pytest.mark.django_db lance une base de données temporaire et isolée pour chaque test — garantissant une indépendance totale entre les tests.
Points clés à retenir
SubFactorycrée automatiquement les objets associés viaForeignKeyà la demande@post_generationgère les relationsManyToManyField, en acceptant une liste d’instances déjà créées- Les fixtures pytest séparent proprement la préparation des données de la logique des assertions
- Faker génère des données diversifiées et réalistes — améliorant la couverture des cas limites
- Les modifications de modèles ne nécessitent des mises à jour que dans les usines — pas dans des dizaines de fichiers de test dispersés
— Editorial Team
Aucun commentaire pour le moment.