FactoryBot 完全指南:Ruby 测试数据工厂的正确用法
FactoryBot 是 Ruby 测试数据管理的事实标准。本文讲解工厂定义、属性序列、关联、trait 变体与 build/create 策略选择,帮你写出可读、可维护、不拖慢测试套的测试数据方案。
FactoryBot 用"工厂"替代 Rails fixtures:每个用例现场构建所需数据,配合序列(sequence)保证唯一性、trait 表达状态变体、关联(association)组装对象图。默认用 build(不落库),只有需要数据库行为时才用 create,这是控制测试速度的关键。
为什么不用 fixtures
Rails 自带的 fixtures 是全局静态数据:所有用例共享同一份"万能数据",改一个字段可能炸掉二十个测试,用例和数据之间隔着文件,读测试时根本看不出它依赖什么。FactoryBot 的思路相反——每个测试按需构建自己需要的数据,依赖关系写在用例里,一眼可见。
安装与第一个工厂
Gemfile 测试组加入 gem 'factory_bot_rails',RSpec 配置 config.include FactoryBot::Syntax::Methods 后即可定义工厂:
FactoryBot.define do
factory :user do
name { 'Alice' }
email { 'alice@example.com' }
admin { false }
end
end
用例中使用:
user = build(:user) # 内存对象,不落库
user = create(:user) # 写入数据库
attrs = attributes_for(:user) # 只拿属性 Hash
create(:user, name: 'Bob') # 现场覆盖属性
四种构建策略的选择
| 策略 | 行为 | 适用场景 |
|---|---|---|
build |
只实例化 | 纯对象逻辑测试,速度最快 |
create |
实例化+落库 | 需要回调、校验、唯一索引、关联查询 |
build_stubbed |
伪造已持久化 | 需要 id 和"已保存"假象,不碰数据库 |
attributes_for |
返回 Hash | 控制器参数测试 |
测试套变慢的元凶往往是无脑 create——先问自己:这个断言真的需要数据库吗?
sequence:唯一性的保障
邮箱、用户名这类带唯一约束的字段,固定值会在第二次 create 时报冲突:
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
sequence(:name) { |n| "用户#{n}" }
end
每次调用自增,永远唯一。
association 与 trait:表达真实数据形态
factory :post do
association :author, factory: :user
title { '默认标题' }
trait :published do
status { 'published' }
published_at { 1.day.ago }
end
trait :with_comments do
after(:create) { |post| create_list(:comment, 3, post: post) }
end
end
create(:post, :published, :with_comments)
association 自动构建关联对象;trait 把"已发布""带评论"这类状态变体命名化,用例意图一目了然;after(:create) 回调处理需要双向关联的复杂场景。
保持测试套健康的纪律
- 工厂保持最小化:默认工厂只填必填字段,特殊状态一律走 trait——肥工厂是隐性耦合的温床;
- 警惕隐式关联雪崩:post 带 author、author 带 team、team 带 organization……一次 create 建十张表,用
build_stubbed或裁剪关联打断链条; - 配合
let惰性构建:RSpec 的let只在引用时触发,避免 before 块里建一堆用不上的数据。
常见问题(FAQ)
Q:FactoryBot 和 fixtures 能混用吗?
可以但别在同一张表上混。常见做法是:枚举型静态数据(国家、角色)用 fixtures,业务对象用 FactoryBot。
Q:测试里 create 报 validation 失败怎么排查?
先在 rails console 里手动跑同样的 FactoryBot.create(:xxx) 复现;十有八九是模型新增校验后工厂没同步更新。养成"加校验时同步检查工厂"的习惯。
Q:create_list 一次建几百条很慢怎么办?
能断言数量就别建满;必须批量时用 create_list(:x, 100) 而非循环 create(内部有批量优化),或改用 insert_all 直接插数据跳过回调。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。