Zpět na domů

Fábry factory_boy v testech Django pytest

Článek demonstruje použití factory_boy pro automatizaci vytváření souvisejících Django objektů v testech pytest. Rozebírá SubFactory pro ForeignKey, post_generation pro ManyToMany a integraci s fixtures. Příklady pokrývají typické scénáře testování modelů knihovny.

Factory_boy: fábry pro spolehlivé Django testy
Advertisement 728x90

Factory Boy pro automatizaci testů Django modelů

Factory Boy zjednodušuje generování propojených objektů Django v testech. Místo ručního vytváření desítek modelů s vazbami ForeignKey a ManyToMany používají factory knihovnu Faker k automatickému generování realistických dat. To urychluje psaní testů, snižuje duplikaci kódu a minimalizuje chyby při úpravách modelů.

Pro ilustraci použijeme modely knihovny: Genre (žánr), Language (jazyk), Author (autor) a Book (kniha se vztahy).

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)
    data_of_birth = models.DateField(null=True, blank=True)
    data_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)

Antipattern: Ruční vytváření objektů

Ruční vytváření propojených objektů vede k následujícím problémům:

Google AdInline article slot
  • Duplikace boilerplate kódu v každém testu
  • Hardcoding hodnot místo dynamické generace
  • Zamíhání přípravy testovacích dat a logiky kontrol
  • Zranitelnost při změnách modelů

Příklad problematického testu:

def test_book_creation():
    genre = Genre.objects.create(name="Sci-fi")
    language = Language.objects.create(name="Čeština")
    author = Author.objects.create(
        first_name="Artem",
        last_name="Strogackij",
        data_of_birth="1925-08-28"
    )
    book = Book.objects.create(
        title="Piknik u cesty",
        genre=genre,
        language=language,
        summary="Jedna z nejznámějších povídek...",
        isbn="9785171180975"
    )
    book.author.add(author)
    assert book.display_author() == "Strogackij"

Tento přístup není škálovatelný pro složité scénáře s desítkami propojených objektů.

Základní factory s Fakerem

Factory Boy se bezproblémově integruje s Django i Fakerem pro generování objektů:

Google AdInline article slot
class AuthorFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Author

    first_name = factory.Faker("first_name")
    last_name = factory.Faker("last_name")
    data_of_birth = factory.Faker("date_time")
    data_of_death = factory.Faker("date_time")

Vytvoření autora se tak zredukuje na jediný řádek:

author = AuthorFactory.create()

Stejně jednoduše funguje i pro ostatní modely:

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")

Zpracování propojených modelů

Pro vazby ForeignKey použijte SubFactory:

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

Pro vazby ManyToMany použijte dekorátor @factory.post_generation:

@factory.post_generation
def author(self, create, extracted, **kwargs):
    if not create or not extracted:
        return
    self.author.add(*extracted)

Test s factory:

def test_book_creation():
    author1 = AuthorFactory.create()
    author2 = AuthorFactory.create()
    book = BookFactory.create(author=[author1, author2])
    assert len(book.author.all()) == 2

Integrace s pytest pomocí fixture

V souboru conftest.py vytvořte fixture pro přípravu dat:

@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

Test v 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)

Dekorátor @pytest.mark.django_db vytvoří dočasnou databázi pro izolaci testů.

Klíčové body

  • SubFactory automaticky vytvoří propojené objekty pro vazby ForeignKey
  • post_generation zpracuje vazby ManyToMany a přijme seznam již existujících objektů
  • Fixture pytest oddělují přípravu dat od kontrol
  • Faker generuje rozmanitá data, což zvyšuje pokrytí edge-case scénářů
  • Úpravy modelů vyžadují úpravy pouze ve factory – nikoli v každém testu

— Editorial Team

Advertisement 728x90

Číst dál