Une factory est une classe utilisée pour générer des instances de modèles contenant des données aléatoires. On se sert de ces classes dans les tests unitaires.
En pré-requis, installer factory_boy
pip install factory_boy
Créer une classe qui hérite de DjangoModelFactory
et déclarer le modèle source dans la sous-classe Meta
core/factories/patient.py:
import factory
class PatientFactory(factory.django.DjangoModelFactory):
class Meta:
model = Patient
created_by = factory.SubFactory('core.factories.user.UserFactory')
Créer une instance de cette classe:
core/tests/models/test_patient:
from django.test import TestCase
from core.factories.user import UserFactory
from core.factories.patient import PatientFactory
class PatientModelTest(TestCase):
def test_auto_computed(self):
"""
Tests if auto fields are correct:
n_studies, last_study
"""
user = UserFactory()
patient = PatientFactory(created_by=user)
self.assertEqual(patient.n_studies, 0)
Plutôt que de laisser la factory créer une valeur aléatoire, on peut passer une valeur différente en paramètre lorsqu’on crée l’instance.
Permet de créer une instance à partir d’une autre factort
created_by = factory.SubFactory('core.factories.user.UserFactory')
Permet de créer une chaîne de caractère contenant 1 mot.
firstname = factory.Faker('word')
Permet de créer une chaîne de caractère contenant plusieurs mots.
name_specify = factory.Faker("sentence", nb_words=3)
allergies_sensitization_comments = factory.Faker("sentence")
Permet de créer une chaîne de caractère contenant un paragraphe
description = factory.Faker("paragraph")
Permet de créer une chaîne de caractère contenant plusieurs paragraphes
cytologic_golde = factory.Faker("text", max_nb_chars=100)
cytologic_other = factory.Faker("text")
Permet de créer une chaîne de caractère parmis une liste de choix possibles.
gender = factory.Iterator(dict(Patient.GENDER_CHOICES).keys())
phone = factory.Iterator(['+33123456789'])
Permet de créer une valeur à partir d’un callback prenant une valeur auto-incrémentée en entrée.
username = factory.Sequence(lambda n: f'User{n}')
from random import randint
def random_rpps_number(nbr):
"""
Function to format an correct RPPS number from a ``factory.Sequence`` number.
"""
range_start = 10 ** (11 - 1)
range_end = (10**11) - 1
return str(randint(range_start, range_end))
rpps_number = factory.Sequence(random_rpps_number)
Permet de créer un uuid4
dicom_study_uid = factory.Faker('uuid4')
Permet de créer un SHA
sha1sum = factory.Faker("sha1")
Permet de créer un type MIME
content_type = factory.Faker("mime_type")
Permet de générer un fichier
from pathlib import Path
import factory.django
PREVIEW_FILE = Path(__file__).parent / "tests" / "resources" / "Fiche intiale BPCO - Phénotyper le patient.jpg"
VIDEO_FILE = Path(__file__).parent / "tests" / "resources" / "Fiche intiale BPCO - Phénotyper le patient.mp4"
file_obj = factory.django.FileField(filename="upload.doc")
preview = factory.django.ImageField(from_path=PREVIEW_FILE)
file_obj = factory.django.FileField(from_path=VIDEO_FILE)
Permet de créer un nombre entier aléatoire
filesize = factory.Faker('random_int', min=10, max=100)
Permet de créer une date aléatoire
mcm_date = factory.Faker('date')
Permet de créer un dateTime entre deux bornes
import datetime
TZ = datetime.timezone.utc
TODAY = datetime.datetime.now(tz=TZ)
PAST_DATE = datetime.datetime(2020, 1, 1, tzinfo=TZ)
submitted_at = factory.Faker("date_time_between", start_date=PAST_DATE, end_date=TODAY, tzinfo=TZ)
Permet de créer une valeur booléenne ou null
nodule = factory.Faker('null_boolean')
Permet de créer une valeur booléenne
factory.Faker('boolean', chance_of_getting_true=20)
Permet d’appeler une méthode du modèle pour récupérer la valeur de l’attribut
password = factory.PostGenerationMethodCall('set_password', 'test')
Permet de référencer un autre attribut
profile = factory.SubFactory("profile.factories.PulmonologistProfileFactory")
consulting_physician_profile = factory.SelfAttribute("profile")
Permet de créer une valeur non pas lorsque l’entité est crée, mais lorsqu’on essaie d’accéder à l’attribut
program_has_been_stopped = factory.LazyAttribute(lambda o: fake.boolean() if not o.final_evaluation_on else False)
name = factory.LazyAttribute(lambda o: f"Factory drug : {fake.sentence(nb_words=2)}")
Utiliser le décorateur @factory.post_generation
pour créer une méthode qui sera appelée après la création de l’instance.
import factory
from core.models.user import User
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
email = factory.Sequence(lambda n: f'user{n}@dacapo.fr')
password = factory.PostGenerationMethodCall('set_password', 'test')
@factory.post_generation
def post_generation(obj, create, extracted, **kwargs):
if obj.is_staff:
if obj.is_staff_privilege is None:
obj.is_staff_privilege = User.STAFF_PRIVILEGE_ROOT
obj.is_superuser = True
else:
obj.is_superuser = (obj.is_staff_privilege == User.STAFF_PRIVILEGE_ROOT)
# will bypass 2 factor authentication when we use force_authenticate
setattr(obj, '_force_device_id', 'BYPASS')
setattr(obj, 'device_id', 'BYPASS')
L’utilisation de Fuzzy est dépréciée
import string
import pytz
import factory.fuzzy
from django.utils import timezone
vidal_id = factory.fuzzy.FuzzyChoice(dict(OCS.OCS_MOLECULE_CHOICES).keys())
efx_power = factory.fuzzy.FuzzyChoice([50, 80, 100, 140, 180, 200, 240, 280])
adeli_number = factory.fuzzy.FuzzyText(length=9, chars=string.digits)
quantity = factory.fuzzy.FuzzyDecimal(0, 1000, precision=2)
duration = factory.fuzzy.FuzzyInteger(1, 30)
inactivity_probability = factory.fuzzy.FuzzyFloat(0, 1)
start_date = factory.fuzzy.FuzzyDate(PAST_DATE, TODAY)
read_information_notice_at = factory.fuzzy.FuzzyDateTime(datetime.datetime(2008, 1, 1, tzinfo=pytz.UTC))
created_at = factory.fuzzy.FuzzyDateTime(timezone.make_aware(datetime.datetime(year=2005, month=1, day=1)))
Il est possible d’ajouter une génération d’attribut personnalisée
Déclarer une classe qui hérite de faker.providers.BaseProvider
core/utils/tests.py:
import datetime
import random
from faker.providers import BaseProvider
randgen = random.Random()
randgen.state_set = False
PAST_DATE = datetime.date(1950, 1, 1)
class DateProvider(BaseProvider):
"""
Date provider
To add this provider to faker:
factory.Faker.add_provider(DateProvider)
To create a date using this provider:
factory.Faker("date_between")
"""
@classmethod
def date_between(cls, start_date, end_date=None):
if end_date is None:
end_date = datetime.date.today()
rand = randgen.randint(start_date.toordinal(), end_date.toordinal())
return datetime.date.fromordinal(rand)
@classmethod
def past_date(cls):
return cls.date_between(PAST_DATE)
Appeler factory.Faker.add_provider
avec cette classe.
Les méthodes déclarées à l’intérieur de la classe pourrons être utilisées pour générer des valeurs d’attribut.
core/factories/patient.py:
import factory
from core.utils.tests import DateProvider
from core.models.patient import Patient
factory.Faker.add_provider(DateProvider)
class PatientFactory(factory.django.DjangoModelFactory):
class Meta:
model = Patient
birth_date = factory.Faker('past_date')
import factory
import factory.fuzzy
from faker.providers import BaseProvider
from asthma.asthma_initial.models.ocs import OCS
class OCSFactory(factory.django.DjangoModelFactory):
id = factory.Sequence(lambda x: x + 1000000)
vidal_id = factory.fuzzy.FuzzyChoice(dict(OCS.OCS_MOLECULE_CHOICES).keys())
quantity = factory.fuzzy.FuzzyDecimal(0, 1000, precision=2)
duration = factory.fuzzy.FuzzyInteger(1, 30)
class Meta:
model = OCS
class OCSListProvider(BaseProvider):
"""
OCS provider
To add this provider to faker:
factory.Faker.add_provider(OCSListProvider)
To create a list of OCS using this provider:
factory.Faker("ocs_list")
"""
def ocs(self):
obj = OCSFactory.build().__dict__
obj.pop("_state")
return obj
def ocs_list(self, min=0, max=3):
n = self.random_int(min=min, max=max)
return [self.ocs() for i in range(n)]
from .ocs import OCSListProvider
factory.Faker.add_provider(OCSListProvider)
class AsthmaInitialTreatmentMixin:
treatment_drugs_ocs_list = factory.Faker("ocs_list")