如何用pytest+factory-boy创建关联外键的Fixture测试销售聚合函数
问题描述
我有一个计算各公司月度销售额的函数aggregate_companies_sales_in_month,想要对该函数进行测试,需要创建测试用的Sales Fixture,具体需求:
- 创建2-3个Company Fixture
- 为这些公司创建多个Sales Fixture
- 使用已创建的Sales Fixture测试聚合函数
另外想知道是否存在类似FuzzyXXX的方法?
已有的代码如下:
聚合函数代码
def aggregate_companies_sales_in_month(): result = ( Sales.objects.all() .annotate(year=TrunkYear('sold_at'), month=TrunkMonth('sold_at')) .values("year", "month", "company") .annotate(sales=Sum("amount")) .values("year", "month", "company", "sales") ) return result
模型定义
class Company(Model): name = CharField(max_length=64) class Sales(Model): sold_at = DatetimeField() company = ForeignKey(Company, on_delete=CASCADE) amount = PositiveIntegerField()
factory-boy工厂定义
class CompanyFactory(DjangoModelFactory): class Meta: model = Company name = faker.company() class SalesFactory(DjangoModelFactory): class Meta: model = Sale sold_at = FuzzyDate(start_date=date(2022,4,1), end_date=date(2022,12,31)) company = Subfactory(CompanyFactory) # maybe should change here? amount = 100
当前测试代码
@pytest.fixture def setup(sales_factory): # This creates sales for 20 companies with sequential ids. sales_factory.create_batch(size=20) @pytest.mark.django_db def test_my_aggregation_function(setup): # create sales fixtures here or take it as argument? actual = aggregate_companies_sales_in_month() ...
解决方案
1. 固定生成2-3个Company Fixture
先定义独立的公司Fixture,确保测试只用到指定的几个公司,避免自动生成大量无关公司:
import pytest from .factories import CompanyFactory, SalesFactory from datetime import date, datetime @pytest.fixture def company_a(): return CompanyFactory(name="A科技有限公司") @pytest.fixture def company_b(): return CompanyFactory(name="B商贸有限公司") # 可选:第三个公司 @pytest.fixture def company_c(): return CompanyFactory(name="C制造有限公司")
2. 关联固定公司生成Sales Fixture
基于上面的公司Fixture,生成对应销售数据,可按月份、金额维度控制数据,方便后续验证聚合逻辑:
全量销售数据Fixture(适合整体测试)
@pytest.fixture def test_sales(company_a, company_b): # A公司5月3笔销售,每笔100元 SalesFactory.create_batch( size=3, company=company_a, sold_at=datetime(2022,5,10,14,0,0), amount=100 ) # A公司6月2笔销售,每笔200元 SalesFactory.create_batch( size=2, company=company_a, sold_at=datetime(2022,6,15,10,0,0), amount=200 ) # B公司5月4笔销售,每笔150元 SalesFactory.create_batch( size=4, company=company_b, sold_at=datetime(2022,5,20,9,0,0), amount=150 ) return Sales.objects.all()
拆分式Fixture(适合单场景测试)
如果需要单独验证某公司某月份的聚合结果,可以拆分Fixture:
@pytest.fixture def company_a_may_sales(company_a): return SalesFactory.create_batch(3, company=company_a, sold_at=datetime(2022,5,10,14,0,0), amount=100) @pytest.fixture def company_b_june_sales(company_b): return SalesFactory.create_batch(2, company=company_b, sold_at=datetime(2022,6,15,10,0,0), amount=250)
3. 编写测试用例验证聚合函数
使用Fixture传入测试数据,断言聚合结果是否符合预期:
@pytest.mark.django_db def test_aggregate_monthly_sales(test_sales, company_a, company_b): actual_result = aggregate_companies_sales_in_month() # 转换结果为字典,用(年,月,公司ID)作为key,方便快速断言 result_map = { (item['year'], item['month'], item['company']): item['sales'] for item in actual_result } # 验证A公司5月销售额:3*100=300 assert result_map[(2022, 5, company_a.id)] == 300 # 验证A公司6月销售额:2*200=400 assert result_map[(2022, 6, company_a.id)] == 400 # 验证B公司5月销售额:4*150=600 assert result_map[(2022, 5, company_b.id)] == 600
关于FuzzyXXX的使用
你已经用到了FuzzyDate,factory-boy提供了多种Fuzzy类型来生成随机测试数据,适配不同场景:
FuzzyInteger:生成指定范围的随机整数,比如amount = FuzzyInteger(50, 500),替代固定金额让测试更灵活FuzzyDateTime:生成指定时间范围的随机datetime(你的Sales.sold_at是DatetimeField,建议换成这个)FuzzyChoice:从给定列表中随机选取值,比如给公司名称限定可选范围:name = FuzzyChoice(["科技公司", "商贸公司", "制造公司"])
修改SalesFactory优化Fuzzy类型使用:
from factory.fuzzy import FuzzyDateTime, FuzzyInteger from datetime import datetime class SalesFactory(DjangoModelFactory): class Meta: model = Sales # 注意你之前代码里写的是Sale,应该和模型名一致改为Sales sold_at = FuzzyDateTime( start_dt=datetime(2022,4,1,0,0,0), end_dt=datetime(2022,12,31,23,59,59) ) company = Subfactory(CompanyFactory) amount = FuzzyInteger(50, 500) # 生成50-500之间的随机金额
如果想让多个Sales记录关联到同一个公司,不要依赖Subfactory自动生成新公司,而是在创建时显式指定已有的company实例,就像前面Fixture中的写法。
内容的提问来源于stack exchange,提问作者muckey
相关产品推荐
相关产品推荐

