使用 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)
反模式:手动创建对象
手动逐个实例化关联对象会带来严重的可维护性问题:
- 每个测试用例中充斥大量重复样板代码
- 硬编码固定值,而非动态生成真实、多样的测试数据
- 测试数据准备逻辑与断言逻辑边界模糊,职责不清
- 测试极其脆弱——模型字段哪怕发生微小变更,也会导致大量测试失败
以下是一段典型的问题代码:
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 深度集成,可稳定生成真实、一致的测试数据:
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:
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
暂无评论。