You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用pytest+factory-boy创建关联外键的Fixture测试销售聚合函数

问题描述

我有一个计算各公司月度销售额的函数aggregate_companies_sales_in_month,想要对该函数进行测试,需要创建测试用的Sales Fixture,具体需求:

  1. 创建2-3个Company Fixture
  2. 为这些公司创建多个Sales Fixture
  3. 使用已创建的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 04:40:23