Retour à l'accueil

Fabriques factory_boy dans les tests Django pytest

L'article démontre l'utilisation de factory_boy pour automatiser la création d'objets Django liés dans les tests pytest. Il couvre SubFactory pour ForeignKey, post_generation pour ManyToMany, et l'intégration avec les fixtures. Les exemples couvrent des scénarios typiques de test de modèles de bibliothèque.

factory_boy : fabriques pour des tests Django fiables
Advertisement 728x90

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é :

Google AdInline article slot
  • 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 :

Google AdInline article slot
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 :

Google AdInline article slot
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

  • SubFactory crée automatiquement les objets associés via ForeignKey à la demande
  • @post_generation gère les relations ManyToManyField, 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

Advertisement 728x90

Lire ensuite