Skip to content

Commit 49e5f83

Browse files
committed
Add FactoryBot-compatible build/create/attributes_for syntax methods
Provide syntactic sugar so users migrating from FactoryBot can use familiar methods like build(:user, :brad), create(:user, :brad), and attributes_for(:user, :brad). These delegate to Rails fixture accessors under the hood, requiring no new loading machinery.
1 parent a9d112b commit 49e5f83

6 files changed

Lines changed: 329 additions & 0 deletions

File tree

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,40 @@ FixtureBot::Schema.define do
422422
end
423423
```
424424

425+
## Migrating from FactoryBot
426+
427+
FixtureBot provides `build`, `create`, `attributes_for`, and related methods that mirror FactoryBot's API. The key difference is that you pass both a table name and a fixture name instead of just a factory name:
428+
429+
```ruby
430+
# FactoryBot # FixtureBot
431+
build(:user) # build(:user, :brad)
432+
create(:user) # create(:user, :brad)
433+
build(:user, name: "X") # build(:user, :brad, name: "X")
434+
attributes_for(:user) # attributes_for(:user, :brad)
435+
build_list(:user, 3) # build_list(:user, :brad, :alice, :bob)
436+
create_list(:user, 3) # create_list(:user, :brad, :alice, :bob)
437+
build_pair(:user) # build_pair(:user, :brad, :alice)
438+
create_pair(:user) # create_pair(:user, :brad, :alice)
439+
build_stubbed(:user) # build_stubbed(:user, :brad)
440+
```
441+
442+
### Method reference
443+
444+
| Method | Behavior |
445+
|---|---|
446+
| `build(:user, :brad, **attrs)` | Duplicates the fixture, applies overrides. Returns unpersisted. |
447+
| `create(:user, :brad, **attrs)` | Without overrides: returns the fixture (already persisted). With overrides: `build` + `save!`. |
448+
| `build_stubbed(:user, :brad, **attrs)` | Like `build` but retains `id`. Looks persisted without touching DB. |
449+
| `attributes_for(:user, :brad, **attrs)` | Returns attributes hash, strips `id`/`created_at`/`updated_at`. |
450+
| `build_list(:user, :brad, :alice, **attrs)` | Maps each name through `build`. |
451+
| `create_list(:user, :brad, :alice, **attrs)` | Maps each name through `create`. |
452+
| `build_pair(:user, :brad, :alice, **attrs)` | Alias for `build_list` with 2 names. |
453+
| `create_pair(:user, :brad, :alice, **attrs)` | Alias for `create_list` with 2 names. |
454+
| `build_stubbed_list(:user, :brad, :alice, **attrs)` | Maps each name through `build_stubbed`. |
455+
| `build_stubbed_pair(:user, :brad, :alice, **attrs)` | Alias for `build_stubbed_list` with 2 names. |
456+
457+
These methods are automatically available in your tests when you require `fixturebot/rspec` or `fixturebot/minitest`. They call the standard Rails fixture accessors under the hood, so `build(:user, :brad)` is equivalent to `users(:brad).dup`.
458+
425459
## Prior art
426460

427461
### [Rails fixtures](https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures)

lib/fixturebot/minitest.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# frozen_string_literal: true
22

33
require "fixturebot/rails"
4+
require "fixturebot/syntax"
45

56
FixtureBot::Rails.compile
7+
8+
ActiveSupport::TestCase.include FixtureBot::Syntax::Methods

lib/fixturebot/rspec.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# frozen_string_literal: true
22

33
require "fixturebot/rails"
4+
require "fixturebot/syntax"
45

56
RSpec.configure do |config|
67
config.before(:suite) do
78
FixtureBot::Rails.compile
89
end
10+
11+
config.include FixtureBot::Syntax::Methods
912
end

lib/fixturebot/syntax.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
require "active_support/core_ext/string/inflections"
4+
require "active_support/core_ext/hash/keys"
5+
6+
module FixtureBot
7+
module Syntax
8+
module Methods
9+
def build(table_name, fixture_name, **attributes)
10+
record = send(table_name.to_s.pluralize, fixture_name).dup
11+
record.assign_attributes(attributes) if attributes.any?
12+
record
13+
end
14+
15+
def create(table_name, fixture_name, **attributes)
16+
if attributes.any?
17+
build(table_name, fixture_name, **attributes).tap(&:save!)
18+
else
19+
send(table_name.to_s.pluralize, fixture_name)
20+
end
21+
end
22+
23+
def build_stubbed(table_name, fixture_name, **attributes)
24+
source = send(table_name.to_s.pluralize, fixture_name)
25+
attrs = source.attributes
26+
attrs.merge!(attributes.stringify_keys) if attributes.any?
27+
source.class.instantiate(attrs)
28+
end
29+
30+
def attributes_for(table_name, fixture_name, **attributes)
31+
send(table_name.to_s.pluralize, fixture_name)
32+
.attributes
33+
.symbolize_keys
34+
.except(:id, :created_at, :updated_at)
35+
.merge(attributes)
36+
end
37+
38+
def build_list(table_name, *fixture_names, **attributes)
39+
fixture_names.map { |name| build(table_name, name, **attributes) }
40+
end
41+
42+
def create_list(table_name, *fixture_names, **attributes)
43+
fixture_names.map { |name| create(table_name, name, **attributes) }
44+
end
45+
46+
def build_pair(table_name, *fixture_names, **attributes)
47+
build_list(table_name, *fixture_names, **attributes)
48+
end
49+
50+
def create_pair(table_name, *fixture_names, **attributes)
51+
create_list(table_name, *fixture_names, **attributes)
52+
end
53+
54+
def build_stubbed_list(table_name, *fixture_names, **attributes)
55+
fixture_names.map { |name| build_stubbed(table_name, name, **attributes) }
56+
end
57+
58+
def build_stubbed_pair(table_name, *fixture_names, **attributes)
59+
build_stubbed_list(table_name, *fixture_names, **attributes)
60+
end
61+
end
62+
end
63+
end

spec/fixturebot/syntax_spec.rb

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
require "fixturebot/syntax"
5+
6+
RSpec.describe FixtureBot::Syntax::Methods do
7+
let(:test_context) do
8+
Object.new.tap do |ctx|
9+
ctx.extend(FixtureBot::Syntax::Methods)
10+
11+
# Stub fixture accessor: users(:brad), users(:alice)
12+
brad = double("brad",
13+
id: 1,
14+
attributes: { "id" => 1, "name" => "Brad", "email" => "brad@example.com", "created_at" => "2024-01-01", "updated_at" => "2024-01-01" },
15+
class: user_class
16+
)
17+
allow(brad).to receive(:dup).and_return(
18+
double("brad_dup",
19+
id: nil,
20+
new_record?: true,
21+
persisted?: false,
22+
class: user_class
23+
).tap do |dup|
24+
allow(dup).to receive(:assign_attributes)
25+
allow(dup).to receive(:save!)
26+
end
27+
)
28+
29+
alice = double("alice",
30+
id: 2,
31+
attributes: { "id" => 2, "name" => "Alice", "email" => "alice@example.com", "created_at" => "2024-01-01", "updated_at" => "2024-01-01" },
32+
class: user_class
33+
)
34+
allow(alice).to receive(:dup).and_return(
35+
double("alice_dup",
36+
id: nil,
37+
new_record?: true,
38+
persisted?: false,
39+
class: user_class
40+
).tap do |dup|
41+
allow(dup).to receive(:assign_attributes)
42+
allow(dup).to receive(:save!)
43+
end
44+
)
45+
46+
allow(ctx).to receive(:users).with(:brad).and_return(brad)
47+
allow(ctx).to receive(:users).with(:alice).and_return(alice)
48+
end
49+
end
50+
51+
let(:user_class) do
52+
klass = double("User")
53+
allow(klass).to receive(:instantiate) do |attrs|
54+
double("stubbed_user", id: attrs["id"], persisted?: true, new_record?: false, **attrs.transform_keys(&:to_sym))
55+
end
56+
klass
57+
end
58+
59+
describe "#build" do
60+
it "returns a dup of the fixture record" do
61+
result = test_context.build(:user, :brad)
62+
expect(result.new_record?).to be true
63+
end
64+
65+
it "applies attribute overrides" do
66+
result = test_context.build(:user, :brad, name: "Changed")
67+
expect(result).to have_received(:assign_attributes).with(name: "Changed")
68+
end
69+
70+
it "does not call assign_attributes when no overrides given" do
71+
result = test_context.build(:user, :brad)
72+
expect(result).not_to have_received(:assign_attributes)
73+
end
74+
end
75+
76+
describe "#create" do
77+
it "returns the original fixture when no overrides given" do
78+
result = test_context.create(:user, :brad)
79+
expect(result.id).to eq(1)
80+
end
81+
82+
it "builds and saves when overrides are given" do
83+
result = test_context.create(:user, :brad, name: "Changed")
84+
expect(result).to have_received(:assign_attributes).with(name: "Changed")
85+
expect(result).to have_received(:save!)
86+
end
87+
end
88+
89+
describe "#build_stubbed" do
90+
it "retains the id from the fixture" do
91+
result = test_context.build_stubbed(:user, :brad)
92+
expect(result.id).to eq(1)
93+
end
94+
95+
it "uses model class instantiate" do
96+
test_context.build_stubbed(:user, :brad)
97+
expect(user_class).to have_received(:instantiate)
98+
end
99+
100+
it "merges attribute overrides" do
101+
result = test_context.build_stubbed(:user, :brad, name: "Stubbed")
102+
expect(result.name).to eq("Stubbed")
103+
end
104+
end
105+
106+
describe "#attributes_for" do
107+
it "returns a hash without id, created_at, updated_at" do
108+
result = test_context.attributes_for(:user, :brad)
109+
expect(result).to eq({ name: "Brad", email: "brad@example.com" })
110+
end
111+
112+
it "merges attribute overrides" do
113+
result = test_context.attributes_for(:user, :brad, name: "Override")
114+
expect(result[:name]).to eq("Override")
115+
end
116+
end
117+
118+
describe "#build_list" do
119+
it "returns a record for each fixture name" do
120+
results = test_context.build_list(:user, :brad, :alice)
121+
expect(results.length).to eq(2)
122+
expect(results).to all(be_truthy)
123+
end
124+
end
125+
126+
describe "#create_list" do
127+
it "returns a record for each fixture name" do
128+
results = test_context.create_list(:user, :brad, :alice)
129+
expect(results.length).to eq(2)
130+
end
131+
end
132+
133+
describe "#build_pair" do
134+
it "returns two records" do
135+
results = test_context.build_pair(:user, :brad, :alice)
136+
expect(results.length).to eq(2)
137+
end
138+
end
139+
140+
describe "#create_pair" do
141+
it "returns two records" do
142+
results = test_context.create_pair(:user, :brad, :alice)
143+
expect(results.length).to eq(2)
144+
end
145+
end
146+
147+
describe "#build_stubbed_list" do
148+
it "returns stubbed records for each fixture name" do
149+
results = test_context.build_stubbed_list(:user, :brad, :alice)
150+
expect(results.length).to eq(2)
151+
expect(results.first.id).to eq(1)
152+
expect(results.last.id).to eq(2)
153+
end
154+
end
155+
156+
describe "#build_stubbed_pair" do
157+
it "returns two stubbed records" do
158+
results = test_context.build_stubbed_pair(:user, :brad, :alice)
159+
expect(results.length).to eq(2)
160+
end
161+
end
162+
end

spec/integration/rails_app_spec.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,68 @@ def run_cmd!(cmd, chdir:, unbundled: true)
157157
expect(File.exist?(File.join(fixtures_dir, "tags.yml"))).to be true
158158
expect(File.exist?(File.join(fixtures_dir, "posts_tags.yml"))).to be true
159159
end
160+
161+
it "supports build, create, and attributes_for via FixtureBot::Syntax::Methods" do
162+
# Write a Minitest test that exercises the syntax methods
163+
test_file = File.join(@app_dir, "test", "syntax_test.rb")
164+
File.write(test_file, <<~RUBY)
165+
require "test_helper"
166+
167+
class SyntaxTest < ActiveSupport::TestCase
168+
fixtures :all
169+
170+
test "build returns unpersisted dup" do
171+
blog = build(:blog, :tech)
172+
assert blog.new_record?, "build should return an unpersisted record"
173+
assert_equal "Tech Blog", blog.title
174+
end
175+
176+
test "build with overrides" do
177+
blog = build(:blog, :tech, title: "Overridden")
178+
assert_equal "Overridden", blog.title
179+
end
180+
181+
test "create without overrides returns persisted fixture" do
182+
blog = create(:blog, :tech)
183+
assert blog.persisted?
184+
assert_equal "Tech Blog", blog.title
185+
end
186+
187+
test "create with overrides saves new record" do
188+
blog = create(:blog, :tech, title: "New Title")
189+
assert blog.persisted?
190+
assert_equal "New Title", blog.title
191+
end
192+
193+
test "attributes_for returns hash without id and timestamps" do
194+
attrs = attributes_for(:blog, :tech)
195+
assert_equal "Tech Blog", attrs[:title]
196+
assert_nil attrs[:id]
197+
assert_nil attrs[:created_at]
198+
assert_nil attrs[:updated_at]
199+
end
200+
201+
test "build_list returns multiple records" do
202+
blogs = build_list(:blog, :tech, :personal)
203+
assert_equal 2, blogs.length
204+
assert blogs.all?(&:new_record?)
205+
end
206+
207+
test "create_list returns multiple persisted records" do
208+
blogs = create_list(:blog, :tech, :personal)
209+
assert_equal 2, blogs.length
210+
assert blogs.all?(&:persisted?)
211+
end
212+
213+
test "build_stubbed retains id and looks persisted" do
214+
blog = build_stubbed(:blog, :tech)
215+
assert_not_nil blog.id
216+
assert_equal "Tech Blog", blog.title
217+
end
218+
end
219+
RUBY
220+
221+
output = run_cmd!("bin/rails test test/syntax_test.rb", chdir: @app_dir)
222+
expect(output).to match(/0 failures/)
223+
end
160224
end

0 commit comments

Comments
 (0)