Pruebas de modelos Django con factory_boy y pytest
Las fábricas de factory_boy simplifican la generación de instancias relacionadas de modelos Django en pruebas. En lugar de crear manualmente decenas de modelos con relaciones ForeignKey y ManyToMany, las fábricas aprovechan Faker para generar automáticamente datos de prueba realistas y variados. Esto acelera la escritura de pruebas, elimina la duplicación de código repetitivo y reduce los fallos cuando los modelos evolucionan.
Veamos un dominio de biblioteca: Genre, Language, Author y Book (con sus relaciones).
class Genre(models.Model):
name = models.CharField(max_length=200)
class Language(models.Model):
name = models.CharField(max_length=20)
class Author(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
date_of_birth = models.DateField(null=True, blank=True)
date_of_death = models.DateField(null=True, blank=True)
class Book(models.Model):
title = models.CharField(max_length=200)
genre = models.ForeignKey('Genre', on_delete=models.CASCADE, null=True)
language = models.ForeignKey('Language', on_delete=models.CASCADE, null=True)
author = models.ManyToManyField('Author')
summary = models.TextField(max_length=1000)
isbn = models.CharField(max_length=13)
El antipatrón: creación manual de objetos
Instanciar manualmente objetos relacionados introduce graves problemas de mantenibilidad:
- Código repetitivo en todas las pruebas
- Valores codificados de forma rígida, en lugar de generación dinámica y realista de datos
- Confusión entre la configuración de datos de prueba y la lógica de aserciones
- Pruebas frágiles que fallan ante cambios mínimos en los modelos
Ejemplo de código problemático:
def test_book_creation():
genre = Genre.objects.create(name="Ciencia ficción")
language = Language.objects.create(name="Ruso")
author = Author.objects.create(
first_name="Arkadi",
last_name="Strugatski",
date_of_birth="1925-08-28"
)
book = Book.objects.create(
title="Picnic a orillas del camino",
genre=genre,
language=language,
summary="Una de las novelas cortas más aclamadas...",
isbn="9785171180975"
)
book.author.add(author)
assert book.display_author() == "Strugatski"
Este enfoque no escala: se vuelve inmanejable y propenso a errores en escenarios complejos con decenas de objetos interrelacionados.
Fábricas básicas con Faker
factory_boy se integra sin esfuerzo con Django y Faker para generar datos de prueba coherentes y realistas:
class AuthorFactory(factory.django.DjangoModelFactory):
class Meta:
model = Author
first_name = factory.Faker("first_name")
last_name = factory.Faker("last_name")
date_of_birth = factory.Faker("date_time")
date_of_death = factory.Faker("date_time")
Ahora crear un autor es tan sencillo como una línea limpia:
author = AuthorFactory.create()
El mismo patrón se aplica a otros modelos:
class GenreFactory(factory.django.DjangoModelFactory):
class Meta:
model = Genre
name = factory.Faker("name")
class LanguageFactory(factory.django.DjangoModelFactory):
class Meta:
model = Language
name = factory.Faker("name")
Manejo de modelos relacionados
Para campos ForeignKey, usa SubFactory:
class BookFactory(factory.django.DjangoModelFactory):
class Meta:
model = Book
title = factory.Faker("sentence")
genre = factory.SubFactory(GenreFactory)
language = factory.SubFactory(LanguageFactory)
summary = factory.Faker("paragraph")
isbn = str(random.randint(1000000000000, 9999999999999))
Para ManyToManyField, usa @factory.post_generation:
@factory.post_generation
def author(self, create, extracted, **kwargs):
if not create or not extracted:
return
self.author.add(*extracted)
Una prueba clara y legible:
def test_book_creation():
author1 = AuthorFactory.create()
author2 = AuthorFactory.create()
book = BookFactory.create(author=[author1, author2])
assert len(book.author.all()) == 2
Integración con pytest mediante fixtures
En conftest.py, define un fixture reutilizable:
@pytest.fixture(scope='function')
def test_book_factory() -> Book:
author1: Author = AuthorFactory.create()
author2: Author = AuthorFactory.create()
book_create: Book = BookFactory.create(author=[author1, author2])
return book_create
Luego, en test_models.py:
@pytest.mark.django_db
def test_model_book(test_book_factory: Book) -> None:
title: str = test_book_factory.title
genre: Genre = test_book_factory.genre
language: Language = test_book_factory.language
summary: str = test_book_factory.summary
isbn: str = test_book_factory.isbn
authors: QuerySet = test_book_factory.author.all()
assert len(summary) <= 1000
assert len(title) <= 200
assert len(isbn) <= 13
assert isinstance(title, str)
assert isinstance(genre, Genre)
assert isinstance(language, Language)
assert isinstance(summary, str)
assert isinstance(isbn, str)
for author in authors:
assert isinstance(author, Author)
El decorador @pytest.mark.django_db inicia una base de datos temporal y aislada para cada prueba, garantizando independencia total entre ellas.
Conclusiones clave
SubFactorycrea automáticamente objetos asociados de tipoForeignKeybajo demanda@post_generationgestiona relacionesManyToManyField, aceptando una lista de instancias ya creadas- Los fixtures de pytest separan con claridad la configuración de datos de la lógica de aserciones
- Faker genera datos diversos y realistas, mejorando la cobertura de casos límite
- Los cambios en los modelos solo requieren actualizaciones en las fábricas — no en docenas de archivos de prueba dispersos
— Editorial Team
Aún no hay comentarios.