Découvrez comment simplifier vos tests grâce aux factories.
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.
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 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"])
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 !
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 !
2022 - tominardi.fr