tominardi.fr

La qualité dans un projet web, Partie 7 - Tester avec des factories

29 mars 2023

Découvrez comment simplifier vos tests grâce aux factories.

Le dossier

Rappel du contexte

Cet article fait partie d'une série d'articles visant à énumérer les différents éléments de qualité à mettre en place au démarrage d'un projet.

C'est le dernier qui va parler de l'outillage à mettre en place autours des tests unitaires.

Introduction

Lorsque l'on écrit des tests unitaires pour une application, on doit souvent créer des instances de modèles pour pouvoir les tester. Cela peut rapidement devenir fastidieux, surtout lorsque l'on a besoin de créer de nombreuses instances pour différents tests. C'est là qu'interviennent les factories.

Une factory est un objet qui permet de générer des instances de modèles de manière programmatique, en spécifiant les valeurs des différents champs. Les factories permettent ainsi de simplifier la création d'instances de modèles pour les tests unitaires.

Factory boy

factory_boy est une bibliothèque Python qui permet de créer des factories pour les tests unitaires. Cette bibliothèque est compatible avec les frameworks de tests les plus populaires, tels que unittest ou pytest. factory_boy fournit également des fonctionnalités avancées telles que la génération de valeurs aléatoires (basé sur l'excellente librairie faker) ou la gestion de dépendances entre les modèles.

Pour commencer à utiliser factory_boy, vous devez l'installer via pip :

pip install factory_boy

Voici un exemple de factory pour un modèle Musician :

import factory
from myapp.models import Musician

class MusicianFactory(factory.Factory):
    class Meta:
        model = Musician

    name = "Fat Mike"
    genre = "Punk rock"

Dans cet exemple, nous créons une factory MusicianFactory qui génère des instances du modèle Musician. Nous spécifions le modèle cible en définissant l'attribut model de la classe Meta.

Nous définissons ensuite les valeurs des différents champs en les définissant directement dans la classe. Dans cet exemple, nous définissons le nom du musicien et son genre avec des valeurs fixes. Cela veut dire que toutes les instances créées avec cette factory auront pour valeurs name="Fat Mike" et genre="Punk rock".

Mais nous pouvons définir des valeurs aléatoires, soit basées sur Faker, soit sur nos propres listes :

import factory
from .models import Musician

class MusicianFactory(factory.Factory):
    class Meta:
        model = Musician

    name = factory.Faker('name')
    genre = factory.Iterator(["Punk rock", "Ska Punk", "Reggae Rock", "Hardcore"])

Utilisation des factories

Une fois que vous avez défini une factory, vous pouvez l'utiliser dans vos tests. Voici un exemple d'utilisation d'une factory dans un test unittest (pour changer un peu de pytest) :

import unittest
from myapp.models import Musician
from myapp.factories import MusicianFactory

class TestMusician(unittest.TestCase):
    def test_create_musician(self):
        musician = MusicianFactory.create()
        self.assertIsInstance(musician, Musician)

Si on a besoin de tester avec des données en particulier, c'est également possible.

import unittest
from myapp.models import Musician
from myapp.factories import MusicianFactory

class TestMusician(unittest.TestCase):
    def test_create_musician(self):
        dub_musician = MusicianFactory.create(genre="Dub")
        # Faites quelque chose de spécifique avec votre musicien de dub

Alors pourquoi utiliser les factories ? Après tout, dans nos exemples, on peut s'en sortir avec le code suivant :

import unittest
from myapp.models import Musician

class TestMusician(unittest.TestCase):
    def test_create_musician(self):
        musician = Musician(name="tata toto", genre="dub")
        # faites quelque chose avec votre musicien

Et bien dans la vraie vie, un modèle, ça va ressembler plutôt à ça :

from django.db import models

class Band(models.Model):
    name = models.CharField(max_length=100)
    city = models.CharField(max_length=100)

class Genre(models.Model):
    name = models.CharField(max_length=100)

class Album(models.Model):
    title = models.CharField(max_length=100)
    release_date = models.DateField()
    band = models.ForeignKey(Band, on_delete=models.CASCADE)
    genre = models.ForeignKey(Genre, on_delete=models.CASCADE)

class Musician(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    instrument = models.CharField(max_length=100)
    birth_date = models.DateField()
    albums = models.ManyToManyField(Album)
    active = models.BooleanField(default=True)
    bio = models.TextField(blank=True)
    website = models.URLField(blank=True)
    photo = models.ImageField(upload_to='musicians/', blank=True)

On a pas mal de champs, obligatoires ou non, typés de manière différente, et surtout, on a des relations à d'autres modèles, qui ont elles-mêmes des relations à d'autres modèles.

Ainsi, à chaque fois qu'on voudra créer un objet pour tester, il faudra préalablement créer le ou les objets liés :

from datetime import date
from django.test import TestCase
from .models import Musician, Album, Band, Genre

class MusicianTestCase(TestCase):

    def setUp(self):
        band = Band.objects.create(name='The Ramones', city='New York')
        genre = Genre.objects.create(name='Punk Rock')
        album = Album.objects.create(title='Ramones', release_date=date(1976, 2, 4), band=band, genre=genre)
        self.musician = Musician.objects.create(first_name='Joey', last_name='Ramone', instrument='vocals', 
                                                birth_date=date(1951, 5, 19), active=True, bio='Joey Ramone was an American musician and singer-songwriter, best known as the lead vocalist of the punk rock band the Ramones.', 
                                                website='http://joeyramone.com/', photo='musicians/joey_ramone.jpg')
        self.musician.albums.add(album)

    def test_musician_creation(self):
        self.assertEqual(self.musician.first_name, 'Joey')
        # Faites les tests que vous voulez avec votre musicien ici

Et ce à chaque endroit où on a juste besoin d'un musicien.

De plus, on est obligé de créer des données qui ne sont pas pertinentes pour notre test. Par exemple dans notre cas nous regardons juste le prénom, alors que nous mettons tout un tas de données non nécessaires dans notre objet.

Avec les factories, on va pouvoir se concentrer sur ce qu'on cherche à tester :

import factory
from .models import Musician, Album, Band, Genre

class BandFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Band

    name = factory.Faker('name')
    city = factory.Faker('city')

class GenreFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Genre

    name = factory.Faker('word')

class AlbumFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Album

    title = factory.Faker('catch_phrase')
    release_date = factory.Faker('date')
    band = factory.SubFactory(BandFactory)
    genre = factory.SubFactory(GenreFactory)

class MusicianFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Musician

    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')
    instrument = factory.Faker('random_element', elements=["Guitar", "Bass", "Drums", "Vocals"])
    birth_date = factory.Faker('date_between', start_date='-50y', end_date='-20y')
    active = factory.Faker('boolean')
    bio = factory.Faker('paragraph')
    website = factory.Faker('url')
    photo = factory.django.ImageField(filename='test.png')

    @factory.post_generation
    def albums(self, create, extracted, **kwargs):
        if not create:
            return
        if extracted:
            for album in extracted:
                self.albums.add(album)
        else:
            for _ in range(random.randint(0, 3)):
                self.albums.add(AlbumFactory.create())
    )

La création des factories est un peu longue, mais c'est du côté des tests qu'on va grandement gagner :

from django.test import TestCase
from .models import Musician
from .factories import MusicianFactory

class MusicianTestCase(TestCase):

    def test_musician_first_name(self):
        musician = MusicianFactory(first_name="Joey")
        self.assertEqual(musician.first_name, "Joey")

Et voilà, avec ça on a un véhicule tout terrain pour faire tout un tas de tests à l'avenir !

Conclusion

C'est tout pour aujourd'hui. Comme d'habitude, je vous encourage à prévoir votre mécanique de Factories au début de votre projet, en fonction de vos choix de technos, de manière à ne pas "pleurer votre race", comme on dit dans le métier, le jour où vous aurez des trucs compliqués à mettre en place.

La prochaine fois, on parlera debugging.

À la revoyure !

Tumblr
Pinterest
LinkedIn
WhatsApp

<< La qualité dans un projet web, Partie 6 - Fixtures et mocks >> La qualité dans un projet web, Partie 8 - Debugging

2022 - tominardi.fr