返回首页

Django pytest 测试中的 factory_boy 工厂

文章演示了使用 factory_boy 在 pytest 测试中自动化创建相关 Django 对象。它涵盖了用于 ForeignKey 的 SubFactory、用于 ManyToMany 的 post_generation,以及与 fixture 的集成。示例涵盖典型的库模型测试场景。

Factory_boy:可靠的 Django 测试工厂
Advertisement 728x90

使用 factory_boy 和 pytest 进行 Django 模型测试

factory_boy 工厂类可大幅简化测试中关联 Django 模型实例的生成过程。无需手动创建数十个含 ForeignKey 和 ManyToMany 关系的模型对象,工厂类借助 Faker 自动生成真实、多样化的测试数据。这不仅显著提升测试编写效率,还能消除重复样板代码,并在模型迭代演进时最大限度降低测试用例的维护成本。

我们以一个图书管理系统为例:Genre(类型)、Language(语言)、Author(作者)和 Book(图书),它们之间存在明确的关联关系。

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)

反模式:手动创建对象

手动逐个实例化关联对象会带来严重的可维护性问题:

Google AdInline article slot
  • 每个测试用例中充斥大量重复样板代码
  • 硬编码固定值,而非动态生成真实、多样的测试数据
  • 测试数据准备逻辑与断言逻辑边界模糊,职责不清
  • 测试极其脆弱——模型字段哪怕发生微小变更,也会导致大量测试失败

以下是一段典型的问题代码:

def test_book_creation():
    genre = Genre.objects.create(name="科幻小说")
    language = Language.objects.create(name="俄语")
    author = Author.objects.create(
        first_name="阿尔卡季",
        last_name="斯特鲁伽茨基",
        date_of_birth="1925-08-28"
    )
    book = Book.objects.create(
        title="路边野餐",
        genre=genre,
        language=language,
        summary="最受赞誉的中篇小说之一……",
        isbn="9785171180975"
    )
    book.author.add(author)
    assert book.display_author() == "斯特鲁伽茨基"

这种写法完全不可扩展——一旦涉及数十个深度嵌套、相互关联的对象,代码将迅速变得臃肿且极易出错。

基于 Faker 的基础工厂类

factory_boy 与 Django 和 Faker 深度集成,可稳定生成真实、一致的测试数据:

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")
    date_of_birth = factory.Faker("date_time")
    date_of_death = factory.Faker("date_time")

现在,只需一行代码即可创建作者实例:

author = AuthorFactory.create()

其他模型同理:

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

处理关联模型

对于 ForeignKey 字段,使用 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))

对于 ManyToManyField,则使用 @factory.post_generation

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

最终得到简洁、可读性强的测试:

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

通过 pytest Fixtures 实现复用

conftest.py 中定义可复用的 fixture:

@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_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)

@pytest.mark.django_db 装饰器为每个测试自动创建一个隔离的临时数据库,确保所有测试彼此完全独立、互不干扰。

核心要点总结

  • SubFactory:按需自动创建关联的 ForeignKey 对象
  • @post_generation:精准处理 ManyToManyField 关系,支持传入已创建好的实例列表
  • pytest fixture 机制清晰分离「测试数据准备」与「业务逻辑断言」
  • Faker 生成丰富、真实的测试数据,大幅提升边界场景覆盖率
  • 模型结构变更时,仅需更新工厂类定义,无需修改散落在数十个测试文件中的硬编码逻辑

— Editorial Team

Advertisement 728x90

继续阅读