← 返回题目列表

测试数据怎么管理?factory 和 fixture 文件该怎么选?

中等 第 20 / 27 题 更新于 2026/08/03
测试数据factory_boyfixture数据隔离测试设计

简化版

测试数据管理的核心矛盾是「每个测试需要不同的数据,但准备数据的代码不该淹没测试意图」。三种主流方案:① JSON/YAML fixture 文件(Django 的 loaddata、手写的 JSON)——看起来整洁但维护性最差:模型加一个非空字段,所有 fixture 文件立刻全部失效;而且从测试里完全看不出它依赖了哪几条数据,久而久之没人敢删。② 工厂(factory_boy / polyfactory)——当前的主流做法:定义一个「合法默认值」的工厂,测试里只写自己关心的字段UserFactory(is_admin=True)),其余自动填充;模型加字段时只改工厂一处。③ 直接在测试里构造——最直白,数据量小时反而最清晰。实践中的推荐是「工厂为主 + 少量真正静态的种子数据用文件」(省份表、币种、权限初始化这类「配置性质」的数据)。除了「怎么造」,还有三个同等重要的问题数据隔离(每个测试自己准备、用事务回滚清理,绝不能依赖前一个测试留下的数据);唯一性冲突(工厂要用 Sequence 生成唯一的 email、用户名,否则并行或多次创建时撞唯一约束);以及「测试意图的可读性」——UserFactory(age=17) 配上变量名 underage_user 比一行 15 个字段的构造清晰得多。一条重要原则测试数据应该「最小且相关」——只创建这个测试真正需要的数据,多余的字段和关联对象不但拖慢测试,还会掩盖真正的依赖关系。核心记忆:工厂为主、文件只用于静态种子只写关心的字段,其余用默认值用 Sequence 防唯一冲突数据最小且相关

详细版

三种方案对比

fixture 文件工厂(factory)测试内构造
模型改字段全部失效只改工厂一处改所有用到的地方
看出依赖看不出✅ 只写关心的✅ 最直白
关联对象手写外键 id✅ SubFactory 自动手动创建
唯一性手动保证✅ Sequence手动
适合静态种子数据✅ 绝大多数场景数据极简时
# ① ★★factory_boy:主流方案★★
import factory
from factory.django import DjangoModelFactory      # 或 factory.alchemy.SQLAlchemyModelFactory

class UserFactory(DjangoModelFactory):
    class Meta:
        model = User
        ★django_get_or_create = ("email",)★         # ★避免重复创建★

    # ★Sequence:保证唯一★
    ★email = factory.Sequence(lambda n: f"user{n}@example.com")★
    ★username = factory.Sequence(lambda n: f"user{n}")★
    # ★Faker:真实感的假数据★
    ★name = factory.Faker("name", locale="zh_CN")★
    age = factory.Faker("pyint", min_value=18, max_value=80)
    # ★LazyAttribute:依赖其他字段★
    ★display = factory.LazyAttribute(lambda o: f"{o.name}({o.username})")★
    # ★LazyFunction:每次调用★
    ★created_at = factory.LazyFunction(timezone.now)★
    is_active = True

class OrderFactory(DjangoModelFactory):
    class Meta:
        model = Order
    ★user = factory.SubFactory(UserFactory)★        # ★★自动创建关联对象★★
    total = factory.Faker("pydecimal", left_digits=4, right_digits=2,
                          positive=True)
    status = "pending"

# ★使用:只写关心的字段★
def test_admin_can_delete():
    admin = ★UserFactory(is_admin=True)★            # ★其余字段自动填充★
    normal = UserFactory()
    assert can_delete(admin, normal) is True

# ★批量★
users = ★UserFactory.create_batch(10)★
# ★不落库(只构造对象)★
user = ★UserFactory.build()★
users = UserFactory.build_batch(5)
# ★只要参数字典★
data = ★factory.build(dict, FACTORY_CLASS=UserFactory)★

# ② ★★Trait:表达"状态变体"★★
class OrderFactory(DjangoModelFactory):
    class Meta:
        model = Order
    status = "pending"
    paid_at = None
    class Params:
        ★paid = factory.Trait(status="paid", paid_at=factory.LazyFunction(now))★
        ★cancelled = factory.Trait(status="cancelled",
                                  cancel_reason="用户取消")★
order = ★OrderFactory(paid=True)★                   # ★★语义清晰★★

# ③ ★★PostGeneration:创建后的额外操作★★
class UserFactory(DjangoModelFactory):
    class Meta:
        model = User
@factory.post_generation★
    def groups(self, create, extracted, **kwargs):
        if not create: return
        if extracted:
            for g in extracted: self.groups.add(g)   # ★多对多★
user = UserFactory(groups=[admin_group, editor_group])

# ★密码要 hash★
    @factory.post_generation
    def password(self, create, extracted, **kwargs):
        self.set_password(extracted or "test1234")
        if create: self.save()

# ④ ★polyfactory:从 Pydantic/dataclass 自动生成★
from polyfactory.factories.pydantic_factory import ModelFactory
class UserCreateFactory(ModelFactory[UserCreate]):
    ★__model__ = UserCreate★                        # ★★字段全自动推导★★
data = UserCreateFactory.build()                   # ★合法的随机数据★
data = UserCreateFactory.build(email="fixed@x.com")

# ⑤ ★★工厂 fixture(pytest 集成)★★
@pytest.fixture
def make_user(db):
    created = []
    def _make(**kwargs):
        u = UserFactory(**kwargs)
        created.append(u)
        return u
    yield _make
    # ★事务回滚的话不需要手动清理★

def test_x(make_user):
    alice = make_user(name="alice")
    admin = make_user(is_admin=True)

# ⑥ ★静态种子数据(少数场景)★
@pytest.fixture(scope="session", autouse=True)
def seed_static_data(django_db_setup, django_db_blocker):
    with django_db_blocker.unblock():
        ★Currency.objects.bulk_create([...])★        # ★★不变的配置数据★★
        Province.objects.bulk_create([...])

⚠️ 三个必须记住的点:① 工厂的核心价值是「测试里只写关心的字段」UserFactory(is_admin=True) 这一行清楚地表达了「这个测试只关心用户是不是管理员」,其余的 email、name、created_at 由工厂填充合法默认值。这带来两个收益:测试的意图一目了然(读的人立刻知道哪个字段是关键),以及模型加字段时只需要改工厂一处——而如果你在 50 个测试里手写 User(email=..., name=..., age=..., is_active=...),加一个非空字段就要改 50 处。② Sequence 是防唯一约束冲突的关键email = factory.Sequence(lambda n: f"user{n}@example.com") 保证每次创建的邮箱都不同——如果写成固定值 "test@example.com",第二次调用工厂就会撞唯一约束,而且这类错误在并行执行(pytest -n auto)下更容易出现。同理适用于用户名、订单号、身份证号等所有唯一字段。③ JSON/YAML fixture 文件的最大问题不是「加载慢」而是「不可维护」。它是数据库某一刻的完整快照,模型加一个非空字段,所有 fixture 文件立刻全部失效(要么报错、要么静默填 null 导致后续断言诡异);更麻烦的是从测试里完全看不出它依赖了文件里的哪几条记录——fixtures = ["all_data.json"] 加载了 200 条数据,某个测试到底依赖哪一条?久而久之没人敢删任何一条,文件越滚越大。它唯一合适的场景是**「真正静态的配置数据」**(省份、币种、权限定义)。

完整版教学

一、三种方案的取舍

★ ★★方案一:fixture 文件(JSON/YAML/SQL)★★
  # Django
  ★python manage.py dumpdata > tests/fixtures/data.json★
  class MyTest(TestCase):
      ★fixtures = ["data.json"]★
  ★ ✓ 优点:
    - ★一次准备,多处复用★
    - ★非开发者也能编辑★
    - 适合"就是这批数据"的场景
  ★ ✗ ★致命缺点★:
    ① ★★模型加非空字段 → 所有文件失效★★
    ② ★★看不出测试依赖哪条数据★★
       fixtures = ["all.json"]   # ★200 条记录,这个测试用哪条?★
       → ★没人敢删,文件只增不减★
    ③ ★加载慢★(每个测试都要 load)
    ④ ★数据和测试分离,改数据要跳文件★
    ⑤ ★重构时 IDE 找不到引用★

★ ★★方案二:工厂(推荐)★★
  ★ ✓ ★只声明关心的字段★
  ★ ✓ ★模型改动只改工厂一处★
  ★ ✓ ★SubFactory 自动处理关联★
  ★ ✓ ★Sequence 保证唯一★
  ★ ✓ ★IDE 能补全和跳转★
  ★ ✗ 要学一点 API
  ★ ✗ ★工厂本身也会腐化★(默认值越加越多)

★ ★方案三:测试内直接构造★
  def test_discount():
      ★items = [Item(price=Decimal("100"), qty=2)]★   # ★极简、极清晰★
      assert calculate_total(items, rate=0) == Decimal("200")
  ★ ✓ ★纯函数测试的最佳选择★(不涉及数据库)
  ★ ✓ 零依赖、零抽象
  ★ ✗ 对象复杂时代码膨胀

★ ★★选择标准★★:
  ┌────────────────────────────────┬──────────────────┐
  │ ★纯函数、值对象、2~3 个字段★     │ ★直接构造★        │
  │ ★需要落库的领域对象★            │ ★★工厂★★          │
  │ ★真正静态的配置数据★            │ ★种子文件/脚本★   │
  │   (省份、币种、权限)           │                   │
  │ ★大量真实数据的性能测试★        │ ★脚本生成★        │
  └────────────────────────────────┴──────────────────┘

★ ★"最小且相关"原则(★最重要的原则★)★:
  ✗ def test_user_can_login():
        user = ★UserFactory(★
            name="张三", age=30, address="...", phone="...",
            company="...", department="...", ...)   # ★★噪音★★
        assert login(user.email, "pw")
  ✓ def test_user_can_login():
        user = ★UserFactory()★                       # ★只要一个合法用户★
        assert login(user.email, "pw")
  ✓ def test_underage_cannot_purchase():
        ★underage = UserFactory(age=17)★             # ★★只写关键字段★★
        assert not can_purchase(underage)
  ★ ★读的人立刻知道:age 是这个测试的关键★
  ★ ★多余的字段会掩盖真正的依赖关系★

★ ★数据量的克制★:
  ✗ ★每个测试都 create_batch(100)★
    → 慢 + 掩盖问题(100 条里哪条触发了 bug?)
  ✓ ★用最少的数据表达场景★
    - 测"列表分页":★创建 3 条,per_page=2★(够验证边界了)
    - 测"排序":★2~3 条足以★
  ✓ ★真的需要大数据量 → 单独的性能测试★

三种方案的选择标准很清晰纯函数和值对象直接构造(最清晰)、需要落库的领域对象用工厂真正静态的配置数据用种子文件fixture 文件的致命缺点不是慢而是不可维护——模型加字段全部失效、看不出测试依赖哪条数据导致没人敢删、重构时 IDE 找不到引用。「最小且相关」是最重要的原则UserFactory(age=17) 让读的人立刻知道「age 是这个测试的关键」,而写满 10 个字段会掩盖真正的依赖关系数据量也要克制——测分页创建 3 条配 per_page=2 就够了,create_batch(100) 既慢又掩盖问题(100 条里哪条触发了 bug?)。

二、工厂的进阶用法

★ ★★声明式字段的几种类型★★:
  ★Sequence★         每次递增(★保证唯一★)
    email = factory.Sequence(lambda n: f"u{n}@x.com")
  ★LazyFunction★     每次调用一个函数(★不依赖其他字段★)
    created = factory.LazyFunction(timezone.now)
  ★LazyAttribute★    ★依赖同一对象的其他字段★
    slug = factory.LazyAttribute(lambda o: slugify(o.title))
    email = factory.LazyAttribute(lambda o: f"{o.username}@x.com")
  ★Faker★            真实感的假数据
    name = factory.Faker("name", locale="zh_CN")
    address = factory.Faker("address")
  ★SubFactory★       ★关联对象(自动创建)★
    author = factory.SubFactory(UserFactory)
  ★SelfAttribute★    引用父工厂的字段
    user = factory.SubFactory(UserFactory,
        name=factory.SelfAttribute("..order.customer_name"))
  ★Iterator★         循环取值
    status = factory.Iterator(["pending", "paid", "shipped"])
  ★Maybe★            条件字段
    paid_at = factory.Maybe("is_paid",
                            yes_declaration=factory.LazyFunction(now),
                            no_declaration=None)

★ ★★build vs create(★重要区别★)★★:
  ★UserFactory.build()★      # ★★只构造对象,不落库★★
  ★UserFactory.create()★     # ★构造 + 保存(★UserFactory() 默认就是 create★)★
  ★UserFactory.stub()★       # ★只有属性的简单对象★
  ★ ✓ ★纯逻辑测试用 build(快得多,不碰数据库)★
  ★ ✗ 注意:★build 时 SubFactory 也不会落库★
    → ★关联对象没有 id,存库时会出错★
    ✓ 用 ★factory.SubFactory(..., strategy=factory.CREATE_STRATEGY)★

★ ★★Trait:表达业务状态(★可读性神器★)★★:
  class Params:
      ★paid = factory.Trait(status="paid", paid_at=LazyFunction(now))★
      ★refunded = factory.Trait(status="refunded",
                               refund_amount=SelfAttribute("total"))★
      ★vip_customer = factory.Trait(
          user=factory.SubFactory(UserFactory, level="vip"))★
  # 使用
  ★OrderFactory(paid=True)★
  ★OrderFactory(paid=True, vip_customer=True)★     # ★可组合★
  ★ ✓ ★比 OrderFactory(status="paid", paid_at=..., ...) 清晰得多★

★ ★工厂继承(表达子类型)★:
  class UserFactory(DjangoModelFactory):
      class Meta: model = User
      is_admin = False
  ★class AdminFactory(UserFactory):★
      is_admin = True
      ★permissions = ["all"]★
  ★ ✓ ★比每次传参数清晰★

★ ★post_generation:创建后的操作★:
  @factory.post_generation
  def tags(self, create, extracted, **kwargs):
      if not create: return                       # ★build 时跳过★
      if extracted:
          self.tags.set(extracted)                # ★多对多★
      else:
          self.tags.add(TagFactory())             # ★默认给一个★
  # ★密码(必须 hash)★
  @factory.post_generation
  def password(self, create, extracted, **kwargs):
      self.set_password(extracted or "test1234")
      if create: self.save()
  ★ ✗ ★注意 post_generation 会触发额外的 save()★

★ ★SQLAlchemy 版本★:
  class UserFactory(★factory.alchemy.SQLAlchemyModelFactory★):
      class Meta:
          model = User
          ★sqlalchemy_session = Session★           # ★要注入 session★
          ★sqlalchemy_session_persistence = "flush"★  # ★或 "commit"★
  # ★配合 pytest fixture 动态注入 session★
  @pytest.fixture(autouse=True)
  def _set_factory_session(db_session):
      UserFactory._meta.sqlalchemy_session = db_session
      OrderFactory._meta.sqlalchemy_session = db_session

★ ★polyfactory(Pydantic/dataclass 首选)★:
  from polyfactory.factories.pydantic_factory import ModelFactory
  class UserFactory(ModelFactory[User]):
      __model__ = User
      # ★不写的字段全部按类型自动生成★
      ★email = lambda: f"{uuid4().hex}@example.com"★   # 覆盖某个字段
  ★ ✓ ★字段全自动推导(连嵌套模型都能生成)★
  ★ ✓ ★适合 FastAPI 项目的 schema★

工厂的声明式字段里几个高频的:Sequence 保证唯一LazyAttribute 依赖同一对象的其他字段slugtitle 生成)、SubFactory 自动创建关联对象Trait 表达业务状态OrderFactory(paid=True) 比传三个字段清晰得多,而且可组合)。buildcreate 的区别很重要——build 只构造对象不落库,纯逻辑测试用它快得多,但要注意 SubFactory 也不会落库导致关联对象没有 id。SQLAlchemy 版本要注入 session(通常用 autouse fixture 动态设置)。Pydantic 和 dataclass 首选 polyfactory——字段全自动推导,连嵌套模型都能生成。

三、数据隔离

★ ★★核心原则:每个测试自己准备、自己不留痕迹★★
  ✗ ★依赖前一个测试留下的数据★
    def test_create_user(): User.objects.create(name="alice")
    def test_list_users(): ★assert User.objects.count() == 1★   # ★依赖顺序★
  ✓ 每个测试独立准备

★ ★三种隔离机制(速度对比)★:
  ┌────────────────────┬────────────┬──────────────────────┐
  │ ★事务回滚★          │ ★★~1ms★★    │ ★★推荐★★             │
  │ ★TRUNCATE 表★       │ ~10~50ms   │ 被测代码有 commit 时  │
  │ ★重建数据库★        │ ~1~5s      │ ★太慢,仅特殊场景★    │
  └────────────────────┴────────────┴──────────────────────┘

★ ★★事务回滚的标准实现★★:
  @pytest.fixture
  def db_session(engine):
      conn = engine.connect()
      ★trans = conn.begin()★                       # ★外层事务★
      session = Session(bind=conn)
      yield session
      session.close()
      ★trans.rollback()★                           # ★★丢弃所有改动★★
      conn.close()
  # ★Django:pytest-django 的 db fixture 默认就是事务回滚★
  # ★FastAPI/SQLAlchemy:如上★

★ ★★被测代码 commit 时的破功问题★★:
  ★ 如果 service 里有 session.commit(),外层事务就提交了 → 回滚失效
  ✓ ★SAVEPOINT 嵌套事务★:
    @pytest.fixture
    def db_session(engine):
        conn = engine.connect(); trans = conn.begin()
        session = Session(bind=conn)
        ★session.begin_nested()★                   # ★SAVEPOINT★
        @event.listens_for(session, "after_transaction_end")
        def restart(sess, t):
            if t.nested and not t._parent.nested:
                ★sess.begin_nested()★              # ★★重新开 SAVEPOINT★★
        yield session
        session.close(); trans.rollback(); conn.close()
  ★ ✓ ★应用的 commit 只提交 SAVEPOINT,外层仍可整体回滚★

★ ★★并行执行下的隔离(xdist)★★:
  @pytest.fixture(scope="session")
  def db_url(★worker_id★):
      if worker_id == "master":
          return BASE_URL
      return f"{BASE_URL}_{worker_id}"             # ★★每 worker 一个库★★
  ★ ✓ Django:pytest-django 会自动加 _gw0 后缀
  ★ ✓ Redis:用不同的 db 号
  ★ ✓ 文件:tmp_path 自动隔离

★ ★★全局状态也要隔离(★常被忽略★)★★:
  @pytest.fixture(autouse=True)
  def _reset():
      yield
      ★cache.clear()★                              # 缓存
      ★app.dependency_overrides.clear()★
      ★SomeSingleton._instance = None★
      ★search_index.clear()★                       # ★搜索索引★
      ★celery_app.conf.task_always_eager = True★
  ★ ★数据库回滚了,但缓存里的旧数据还在 → 下个测试读到脏数据★

★ ★静态种子数据的处理★:
  ★ 问题:币种、省份这类"每个测试都需要但从不改变"的数据
  ✓ ★session 级创建一次,且不在事务里★:
    @pytest.fixture(scope="session", autouse=True)
    def seed(django_db_setup, django_db_blocker):
        with django_db_blocker.unblock():
            Currency.objects.bulk_create([...])
  ✓ ★或者放进迁移(data migration)★
  ★ ✗ 不要在每个测试的 fixture 里重复创建

★ ★测试之间的"时间"隔离★:
  ✗ 某个测试用 freeze_time 改了时间但没还原
  ✓ ★用装饰器或上下文管理器(自动还原)★
  ✓ ★autouse fixture 兜底★

数据隔离的核心原则是「每个测试自己准备、自己不留痕迹」。三种机制里事务回滚最快(约 1ms)也最推荐,但被测代码里有 commit() 时会破功——解法是 SAVEPOINT 嵌套事务(监听 after_transaction_end 重新开 SAVEPOINT,让应用的 commit 只提交 SAVEPOINT)。并行执行下要按 worker_id 给每个 worker 独立的数据库最容易被忽略的是「全局状态也要隔离」——数据库回滚了,但缓存里的旧数据还在,下一个测试就读到脏数据;同样需要清理的还有 dependency_overrides、单例、搜索索引。静态种子数据要 session 级创建一次且不在事务里,或者干脆放进 data migration。

四、可读性与维护性

★ ★★测试数据应该"讲故事"★★:
  ✗ def test_x():
        u = UserFactory(age=17, is_verified=False, balance=0)
        o = OrderFactory(user=u, total=100)
        assert process(o).status == "rejected"
        # ★★读的人:到底哪个字段导致了 rejected?★★
  ✓ def test_underage_user_order_rejected():
        ★underage_user = UserFactory(age=17)★       # ★★变量名 + 关键字段★★
        order = OrderFactory(user=underage_user)
        assert process(order).status == "rejected"
        # ★一眼看出:因为未成年★

★ ★三条可读性规则★:
  ① ★★变量名说明"这是什么角色"★★
     admin / underage_user / expired_coupon / oversized_file
  ② ★★只写与断言相关的字段★★
  ③ ★★用 Trait 或子工厂表达状态★★
     OrderFactory(paid=True)  >  OrderFactory(status="paid", paid_at=...)

★ ★★Arrange-Act-Assert 的结构★★:
  def test_discount_applied():
      # ★Arrange:准备(★最小化★)★
      user = UserFactory(level="vip")
      cart = CartFactory(user=user, items=[ItemFactory(price=100)])
      # ★Act:执行(★一行★)★
      ★result = checkout(cart)★
      # ★Assert:断言(★聚焦★)★
      assert result.total == Decimal("90")
  ★ ✓ ★空行分隔三段,结构一目了然★

★ ★★工厂的腐化(★要警惕★)★★:
  # ★半年后的工厂★
  class UserFactory(DjangoModelFactory):
      name = ...
      email = ...
      ★# 下面 30 个字段是各种测试陆续加的★
      preference_json = ...
      last_login_ip = ...
      ★avatar = factory.django.ImageField()★        # ★★每次都生成图片!★★
      ★subscription = factory.SubFactory(SubFactory)★  # ★★每次都建关联★★
  ★ ✗ 后果:★创建一个用户要几百毫秒 + 建 5 个关联对象★
  ✓ ★对策★:
    - ★默认值保持最小★(只填必填字段)
    - ★可选的关联用 Trait 或 post_generation 按需创建★
    - ★定期审查工厂的成本★(--durations 里 setup 慢就看它)

★ ★★避免"共享的可变测试数据"★★:
  ✗ # conftest.py
    ★SHARED_USER = UserFactory()★                  # ★★模块级★★
    def test_a(): SHARED_USER.name = "changed"     # ★污染★
    def test_b(): assert SHARED_USER.name == "..."  # ★★依赖顺序★★
  ✓ ★用 fixture(每个测试新建)★
  ✓ ★或者用不可变的常量★

★ ★★工厂 vs fixture:怎么配合★★:
  # ★fixture 负责"这个测试用的具体对象"★
  @pytest.fixture
  def admin_user(db):
      return ★UserFactory(is_admin=True)★
  # ★工厂负责"怎么造一个合法对象"★
  # ★工厂 fixture 负责"让测试能自己造"★
  @pytest.fixture
  def make_order(db):
      def _make(**kw): return OrderFactory(**kw)
      return _make
  ★ ★三者配合:★
    - ★多个测试共用同一个对象 → fixture★
    - ★每个测试要造不同的 → 工厂 fixture★
    - ★纯逻辑测试 → 直接调工厂的 build()★

★ ★数据构造的性能★:
  ★ 每个 SubFactory 都是一次额外的 INSERT
    OrderFactory() → ★创建 Order + User + Address + ...★
  ✓ ★复用已有对象★:
    user = UserFactory()
    orders = ★OrderFactory.create_batch(5, user=user)★   # ★★只建一个用户★★
  ✓ ★用 build() 避免落库★
  ✓ ★批量创建用 create_batch(内部可能有优化)★

测试数据应该「讲故事」——underage_user = UserFactory(age=17) 让读的人一眼看出「因为未成年所以被拒绝」,而写满字段会让人猜不出哪个是关键。三条可读性规则:变量名说明角色、只写与断言相关的字段、用 Trait 表达状态要警惕「工厂的腐化」——半年后各种测试陆续往工厂里加了 30 个字段,其中 ImageField 每次生成图片、SubFactory 每次建关联对象,创建一个用户要几百毫秒;对策是默认值保持最小、可选关联用 Trait 按需创建、定期审查工厂成本工厂和 fixture 的配合:多个测试共用同一个对象用 fixture、每个测试要造不同的用工厂 fixture、纯逻辑测试直接用 build()

五、特殊场景

★ ★① 真实感数据(Faker)★:
  from faker import Faker
  fake = Faker("zh_CN")
  fake.name() / fake.address() / fake.company()
  fake.phone_number() / fake.email() / fake.ssn()
  ★ ✓ ★演示、截图、性能测试时有用★
  ★ ✗ ★单元测试里"真实感"没有价值★——反而降低可读性
    ✗ user = UserFactory()   # name="张伟" —— ★这个名字重要吗?★
    ✓ 断言相关的字段用明确的值
  ★ ★固定种子保证可复现★:Faker.seed(0)

★ ★★② 敏感数据(★绝不能用真实数据★)★★:
  ✗ ★从生产库导数据到测试环境★
    → ★★违反隐私法规(GDPR/个保法)+ 泄露风险★★
  ✓ ★用工厂生成★
  ✓ ★必须用生产数据形态时 → 脱敏★:
    - ★姓名/手机/邮箱/身份证 → 替换★
    - ★保留统计特征(长度分布、格式)★
    - ★不可逆(不能只是简单替换)★
  ✓ ★用专门的脱敏工具或生成合成数据★

★ ★③ 大数据量(性能测试)★:
  ✗ 用工厂逐个创建 10 万条(★几分钟★)
  ✓ ★bulk_create / COPY★:
    Model.objects.bulk_create(
        [Model(**d) for d in data], ★batch_size=1000★)
  ✓ ★PostgreSQL 的 COPY★(最快)
  ✓ ★或者预先准备好的数据库快照/dump★
  ★ ★性能测试的数据准备应该和功能测试分开★

★ ★④ 时间相关的数据★:
  ✗ ★created_at = datetime(2024, 1, 1)★           # ★★硬编码,会过期★★
    assert obj.is_recent()                        # ★两年后就挂了★
  ✓ ★相对时间★:
    created_at = factory.LazyFunction(
        lambda: timezone.now() - timedelta(days=1))
  ✓ ★或冻结时间★:
    @freeze_time("2026-01-15")
    def test_x(): ...
  ★ ★原则:要么全部相对"现在",要么全部冻结★

★ ★⑤ 依赖外部 ID 的数据★:
  ✗ ★user_id = 1★                                 # ★假设数据库是空的★
  ✓ ★user = UserFactory(); order.user_id = user.id★
  ★ ★永远不要假设自增 ID 的具体值★

★ ★⑥ 多租户/多语言数据★:
  class TenantFactory(DjangoModelFactory):
      name = factory.Sequence(lambda n: f"tenant{n}")
  class UserFactory(DjangoModelFactory):
      ★tenant = factory.SubFactory(TenantFactory)★
  # ★测试租户隔离★
  def test_tenant_isolation():
      t1, t2 = TenantFactory(), TenantFactory()
      ★u1 = UserFactory(tenant=t1)★
      ★u2 = UserFactory(tenant=t2)★
      assert list(users_of(t1)) == [u1]           # ★★关键断言★★

★ ★⑦ 状态机数据(各种状态的对象)★:
  # ★用 Trait 组合出各状态★
  class Params:
      draft = factory.Trait(status="draft")
      submitted = factory.Trait(status="submitted",
                                submitted_at=LazyFunction(now))
      approved = factory.Trait(status="approved",
                               approver=factory.SubFactory(UserFactory))
  # ★参数化测试各状态的行为★
  @pytest.mark.parametrize("state,can_edit", [
      ("draft", True), ("submitted", False), ("approved", False)])
  def test_edit_permission(state, can_edit):
      doc = DocFactory(★**{state: True}★)
      assert doc.can_edit() is can_edit

特殊场景里几个关键点:Faker 的「真实感」在单元测试里没有价值name="张伟" 对测试逻辑毫无意义,反而降低可读性),它适合演示和性能测试。敏感数据绝不能从生产库导——违反隐私法规且有泄露风险,必须用工厂生成或做不可逆的脱敏大数据量用 bulk_createCOPY,而且性能测试的数据准备应该和功能测试分开时间相关的数据不要硬编码datetime(2024,1,1) 两年后测试就挂了)——要么全部相对「现在」、要么全部冻结永远不要假设自增 ID 的具体值

六、实践清单

★ ★推荐的组织结构★:
  tests/
  ├── conftest.py            # ★全局 fixture★
  ├── ★factories.py★          # ★★所有工厂集中定义★★
  ├── helpers.py             # 断言辅助、工具函数
  ├── fixtures/              # ★仅静态种子数据★
  │   └── currencies.json
  └── unit/ integration/ ...

  # factories.py
  class UserFactory(DjangoModelFactory): ...
  class OrderFactory(DjangoModelFactory): ...
  # conftest.py
  @pytest.fixture
  def admin_user(db): return UserFactory(is_admin=True)

★ 检查清单:
  【方案选择】
  □ ★工厂为主,文件只用于静态种子数据★
  □ ★纯函数测试直接构造对象(不用工厂)★
  □ ★工厂集中在 factories.py★
  【工厂设计】
  □ ★唯一字段用 Sequence★
  □ ★默认值保持最小(只填必填)★
  □ ★可选关联用 Trait/post_generation 按需创建★
  □ ★状态变体用 Trait 而不是传一堆字段★
  □ ★纯逻辑测试用 build() 不落库★
  【隔离】
  □ ★每个测试自己准备数据★
  □ ★事务回滚做隔离(有 commit 时用 SAVEPOINT)★
  □ ★缓存/单例/overrides 也要清理★
  □ ★并行下按 worker 隔离数据库★
  【可读性】
  □ ★变量名表达角色(underage_user)★
  □ ★只写与断言相关的字段★
  □ ★Arrange-Act-Assert 结构清晰★
  □ ★数据量最小(测分页 3 条就够)★
  【安全】
  □ ★绝不使用生产数据★
  □ ★工厂不生成真实的手机号/身份证(用假格式)★

★ ★★常见问题速查★★:
  ┌────────────────────────────────────┬──────────────────┐
  │ UNIQUE constraint failed            │ ★没用 Sequence★   │
  │ 测试单跑过全跑挂                     │ ★数据没隔离★      │
  │ setup 很慢                          │ ★工厂腐化/关联太多★│
  │ 改模型后一堆测试挂                   │ ★用了 fixture 文件★│
  │ 并行时数据串                        │ ★没按 worker 隔离★│
  │ 两年后测试突然挂                     │ ★硬编码了日期★    │
  │ 数据库回滚了但结果还是脏的           │ ★缓存没清★        │
  └────────────────────────────────────┴──────────────────┘

★ 一句话总结:
  ★"测试数据用工厂为主(只写关心的字段、其余自动填充,
    模型改动只改一处),JSON fixture 文件只留给真正静态的种子数据;
    唯一字段一定要用 Sequence 防冲突,状态变体用 Trait 表达;
    隔离靠事务回滚(有 commit 就用 SAVEPOINT),
    别忘了缓存和单例也要清;
    最重要的原则是『最小且相关』——只造这个测试真正需要的数据。"★

推荐的组织结构是工厂集中在 factories.py、fixture 在 conftest.py、静态种子数据单独放目录。问题速查表里几条高频的:UNIQUE constraint failed 是没用 Sequencesetup 很慢是工厂腐化或关联太多改模型后一堆测试挂是用了 fixture 文件数据库回滚了但结果还是脏的是缓存没清

记忆钩子:「测试数据管理的核心矛盾是★『每个测试需要不同的数据,但准备数据的代码不该淹没测试意图』★。三种方案:★① JSON/YAML fixture 文件——看起来整洁但维护性最差★:★模型加一个非空字段所有文件立刻失效★、★从测试里完全看不出它依赖哪几条数据导致没人敢删、文件只增不减★、加载慢、IDE 找不到引用 → ★只适合真正静态的配置数据(省份、币种、权限)★;★② 工厂(factory_boy / polyfactory)——当前主流★:★测试里只写自己关心的字段(UserFactory(is_admin=True)),其余自动填充合法默认值★,★模型加字段只改工厂一处★(对比手写要改 50 处);★③ 纯函数和值对象直接构造最清晰★。★工厂的关键 API★:★Sequence 保证唯一(写成固定值第二次创建就撞唯一约束,并行下更容易暴露)★、★LazyAttribute 依赖同对象的其他字段★、★SubFactory 自动创建关联★、★Trait 表达业务状态且可组合(OrderFactory(paid=True) 远好过传三个字段)★、★build() 只构造不落库(纯逻辑测试快得多,但 SubFactory 也不落库导致关联对象没 id)★。★要警惕工厂腐化★——半年后各种测试往里加了 30 个字段,★ImageField 每次生成图片、SubFactory 每次建关联,创建一个用户几百毫秒★ → ★默认值保持最小、可选关联用 Trait 按需创建★。★数据隔离★:★事务回滚约 1ms 最推荐,但被测代码有 commit 会破功 → 用 SAVEPOINT 嵌套事务★(监听 after_transaction_end 重新开);★并行下按 worker_id 给每个 worker 独立数据库★;★最容易忽略的是『数据库回滚了但缓存里的旧数据还在』——缓存/单例/dependency_overrides 都要清★;★静态种子数据 session 级创建一次且不在事务里★。★最重要的原则是『最小且相关』★:★UserFactory(age=17) 配变量名 underage_user 让人一眼看出关键★,★多余字段会掩盖真正的依赖关系★;★数据量也要克制——测分页 3 条配 per_page=2 就够,create_batch(100) 既慢又掩盖问题★。其他要点:★时间数据别硬编码(datetime(2024,1,1) 两年后会挂),要么全部相对『现在』要么全部冻结★、★永远不要假设自增 ID 的具体值★、★绝不能从生产库导数据(违反隐私法规)★、★Faker 的『真实感』在单元测试里没价值反而降低可读性★。」

七、常见误区与追问

  • 误区:用 dumpdata 导出一份完整的测试数据 JSON,所有测试共用,最省事。 短期最省事,长期是维护灾难。三个问题会逐渐浮现:① 模型演进即失效——给模型加一个非空字段(没有默认值),所有 fixture 文件立刻全部报错;就算加了默认值,那些文件里的数据也永远停留在旧的形态。② 依赖关系不可见——fixtures = ["all_data.json"] 加载了 200 条记录,但某个具体的测试到底依赖哪一条?没人说得清,于是没人敢删任何一条数据,文件只增不减,最后变成一个几千行、包含大量无用数据的巨型文件,每个测试都要加载它。③ 重构不友好——改字段名时 IDE 的重命名功能不会碰 JSON 文件,只能全文搜索。工厂方案则把「数据的形状」和「数据的值」分开了:形状由工厂统一维护(改一处),值由每个测试按需指定(意图清晰)。JSON fixture 唯一合适的场景是「真正静态的配置数据」——币种、省份、权限定义这类几年不变、且本质上是「配置」而不是「测试场景」的数据。
  • 误区:工厂的默认值写得越全越好,这样测试里就什么都不用写了。 默认值越多,工厂越慢、测试越不透明。常见的腐化路径:某个测试需要用户有头像,于是往工厂里加了 avatar = factory.django.ImageField()——从此每次创建用户都会生成一张图片;另一个测试需要订阅信息,加了 subscription = factory.SubFactory(SubscriptionFactory)——从此每次创建用户都会多插一条订阅记录(而那条记录又可能带出更多关联)。半年后,UserFactory() 一次调用要几百毫秒、插入五张表。正确的做法是「默认值只填必填字段」(数据库层面 NOT NULL 且没有默认值的),可选的关联和重资源用 Traitpost_generation 按需创建——UserFactory(with_avatar=True)。判断信号:pytest --durations 里如果大量测试的 setup 时间接近,去看工厂就对了。
  • 误区:所有唯一字段随便写个固定值就行,反正每个测试都会回滚。 同一个测试里创建两个对象就会撞唯一约束email = "test@example.com" 这样的写法,UserFactory() 调用第一次没问题,第二次就报 UNIQUE constraint failed——而「一个测试里创建两个用户」是极常见的场景(测试权限、测试列表、测试关联)。正确做法是用 factory.Sequence(lambda n: f"user{n}@example.com"),factory_boy 会维护一个自增计数器保证每次不同。需要注意几点:① Sequence 的计数器在整个进程内递增,所以并行(xdist)时不同 worker 各有各的计数器——如果它们共用一个数据库就还是会冲突,所以并行时数据库也要按 worker 隔离② 如果测试之间不回滚(比如用了 TRUNCATE 方案),计数器和实际数据可能不同步;③ 复合唯一约束unique_together)需要保证组合唯一,可能要用 LazyAttribute 结合多个字段。
  • 误区:数据库用了事务回滚,测试之间就完全隔离了。 只隔离了数据库,其他状态照样会串。最常见的漏网之鱼是缓存:测试 A 查询了某个用户,结果被缓存进 Redis 或进程内存;测试 A 的数据库改动回滚了,但缓存里那条记录还在——测试 B 读到的是一条「数据库里已经不存在的数据」,表现为莫名其妙的断言失败,而且单跑测试 B 时完全正常。同类的还有:搜索索引(Elasticsearch 的写入不会跟着数据库事务回滚)、app.dependency_overrides(FastAPI)、单例对象的内部状态模块级的缓存字典freeze_time 没还原环境变量被改了。解法是一个 autouse=True 的清理 fixture,在 teardown 里把这些统统重置。还有一个更隐蔽的:被测代码里的 commit() 会让外层事务提交,导致回滚失效——需要用 SAVEPOINT 嵌套事务方案。
  • 误区:测试数据用 Faker 生成真实感的姓名和地址,测试更接近生产。 在单元测试里,「真实感」不但没有价值,还会降低可读性UserFactory() 生成一个叫「张伟」、住在「上海市浦东新区xx路」的用户——这些信息对测试逻辑毫无意义,读测试的人还要花一秒钟判断「这个名字重要吗」。更糟的是,如果断言依赖了随机生成的值,测试就变得不可预测(Faker 每次生成不同,偶尔会生成包含特殊字符的名字导致失败——这时它其实变成了低配版的属性测试,但没有收缩能力,只会给你一个 flaky test)。正确的用法是:与断言相关的字段用明确的字面量UserFactory(age=17)),不相关的字段用什么都行(Faker 或简单的 Sequence 都可以)。Faker 真正有价值的场景是演示环境的数据填充、性能测试的大批量数据、以及需要「多样化输入」的探索性测试(那种场景要固定 Faker.seed() 保证可复现)。
  • 追问:factory_boy 和 polyfactory 该怎么选?你的数据模型是什么factory_boy 更适合 ORM 模型——它对 Django 的 DjangoModelFactory 和 SQLAlchemy 的 SQLAlchemyModelFactory 有专门支持,处理了「创建时落库」「关联对象」「多对多」「post_save 钩子」这些 ORM 特有的复杂性,而且 TraitSubFactorypost_generation 这套 API 很成熟,社区积累多。polyfactory 更适合 Pydantic 模型和 dataclass——它的最大优势是从类型注解自动推导所有字段,你只需要写 __model__ = User 就能生成完全合法的实例(包括嵌套模型、LiteralEnum、约束条件如 Field(ge=0, le=100) 都能正确处理),几乎不用写字段定义。所以在 FastAPI 项目里,测试请求体和响应模型用 polyfactory、测试数据库模型用 factory_boy 是很自然的组合。两者也可以共存。如果项目里既有 Pydantic schema 又有 SQLAlchemy model,通常的做法是给 model 写 factory_boy 工厂(因为要落库),给 schema 用 polyfactory(因为只是构造对象)
  • 追问:build()create() 具体差别在哪,什么时候用 build? create()(也就是直接调用 UserFactory())会构造对象并保存到数据库;build() 只构造内存中的对象,不发任何 SQL。所以 build() 快得多——纯粹的对象构造是微秒级,而落库要走数据库往返。适用场景是测试不碰数据库的纯逻辑:比如验证一个序列化器的输出格式、测试一个计算函数、检查模型的 __str__ 方法。但有两个坑要注意:SubFactory 也不会落库——OrderFactory.build() 里的 user 是一个未保存的 User 实例,它的 idNone,如果你之后想保存这个 order 就会失败(外键约束);需要「对象不落库但关联落库」时要显式指定 SubFactory(..., strategy=factory.CREATE_STRATEGY)post_generation 钩子在 build 时通常被跳过(多对多关系没法在未保存的对象上设置),所以工厂里的 post_generation 要写 if not create: return。还有第三个变体 stub()——它返回一个只有属性的简单对象(不是模型实例),适合完全不需要模型行为的场景。
  • 追问:能不能把生产数据脱敏后拿来测试? 技术上可以,但要非常谨慎,而且通常不是好主意。风险有三:① 合规风险——GDPR、个人信息保护法对个人数据的处理有严格要求,「导到测试环境」通常需要合法性基础和相应的安全措施;一旦测试环境泄露(而测试环境的防护通常弱得多),后果很严重。② 脱敏难以彻底——简单替换姓名手机号是不够的:准标识符的组合仍可能重识别(出生日期 + 邮编 + 性别常能唯一定位一个人)、自由文本字段(备注、地址、工单内容)里可能有身份信息、关联表里的数据可能反推出原始信息。③ 数据形态会漂移——今天导的数据到明年就和线上不一致了,还要重新导。更好的方案是:用工厂生成合成数据(可控、可复现、无合规风险),如果需要「生产数据的统计特征」(比如测试查询性能),就生成符合同样分布的合成数据而不是搬运真实数据。确实需要用真实数据排查特定问题时,应该走受控的、有审批和审计的流程,用完即删。

八、加强记忆

测试数据管理的核心矛盾是**「每个测试需要不同的数据,但准备数据的代码不该淹没测试意图」。三种方案:① JSON/YAML fixture 文件——看起来整洁但维护性最差模型加一个非空字段所有文件立刻失效从测试里完全看不出它依赖哪几条数据,导致没人敢删、文件只增不减、加载慢、IDE 找不到引用——它只适合真正静态的配置数据(省份、币种、权限定义);② 工厂(factory_boy / polyfactory)——当前的主流做法测试里只写自己关心的字段UserFactory(is_admin=True)),其余自动填充合法默认值,模型加字段时只改工厂一处(对比手写要改 50 处);③ 纯函数和值对象直接构造最清晰工厂的关键 APISequence 保证唯一**(写成固定值时第二次创建就撞唯一约束,并行下更容易暴露)、LazyAttribute 依赖同一对象的其他字段SubFactory 自动创建关联对象Trait 表达业务状态且可组合OrderFactory(paid=True) 远好过传三个字段)、build() 只构造不落库(纯逻辑测试快得多,但要注意 SubFactory 也不落库导致关联对象没有 id)。要警惕「工厂腐化」——半年后各种测试往工厂里加了 30 个字段,ImageField 每次生成图片、SubFactory 每次建关联,创建一个用户要几百毫秒 → 对策是默认值保持最小、可选关联用 Trait 按需创建数据隔离事务回滚约 1ms 最推荐,但被测代码里有 commit() 时会破功 → 要用 SAVEPOINT 嵌套事务并行时按 worker_id 给每个 worker 独立的数据库最容易被忽略的是「数据库回滚了但缓存里的旧数据还在」——缓存、单例、dependency_overrides、搜索索引都要清理;静态种子数据要 session 级创建一次且不在事务里最重要的原则是「最小且相关」UserFactory(age=17) 配上变量名 underage_user 让人一眼看出关键多余的字段会掩盖真正的依赖关系数据量也要克制——测分页创建 3 条配 per_page=2 就够了,create_batch(100) 既慢又掩盖问题。其他要点:时间数据别硬编码datetime(2024,1,1) 两年后测试就会挂),要么全部相对「现在」、要么全部冻结永远不要假设自增 ID 的具体值绝不能从生产库导数据(违反隐私法规且脱敏难以彻底);Faker 的「真实感」在单元测试里没有价值,反而降低可读性