disclaimer: Always try making the object graph less complex first.
[ed. I've renamed this to the FieldHand pattern, to avoid naming colisions]
The Problem: How do you build even moderatly complicated object graphs for request specs? Three things you don't want to do:
- Duplicate the setup, and linking of multple objects across different files.
- Have very long test files to avoid duplicate setup ( a.k.a fixing (1) the lazy way )
- Extracting
letcreation into a module. This may seem like a good idea at first, but it quickly becomes very hard to override the rightlet, and even harder to figure out what your test is doing.
A Solution: Follow these simple rules:
- Extract model creation to methods in a module
- You are allowed to supply default attributes to
create! yieldback to the caller.- You are not allowed to put any
lets in the module - In the spec, call your extracted methods
- In the spec, if you need a "sticky" instance, wrap a call to your extracted method in a
let - Construct the object graph from method calls and
lets in the spec.
Was:
# spec/requests/drawing_issues_notices/create_drawing_issue_notice_spec.rb
before do
post "/drawing_issue_notices", drawing_issue_notice: din_attrs
# inside `#create` send an email to the drawing_issue_notice->primary_model->primary_engineer->email
end
let(:din_attrs) do
default_drawing_issue_notice_attributes
.merge(
primary_platform_id: platform.id,
)
end
let(:pic) do
Engineer.create! { |e| e.email = SecureRandom.uuid }
end
let(:platform) do
Platform.create! { |p| p.primary_engineer_id = pic.id }
endBecomes:
# spec/requests/drawing_issues_notices/create_drawing_issue_notice_spec.rb
include SpecSupportDrawingIssueNotices
before do
post "/drawing_issue_notices", drawing_issue_notice: din_attrs
# inside `#create` send an email to the drawing_issue_notice->primary_model->primary_engineer->email
end
let(:din_attrs) do
default_drawing_issue_notice_attributes
.merge(
primary_platform_id: platform.id,
)
end
let(:pic) { din_engineer }
let(:platform) do
din_platform { |p| p.primary_engineer_id = pic.id }
end# spec/support/drawing_issue_notices.rb
module_function
def din_engineer opts={}
Engineer.create!(opts) do |e|
e.email = SecureRandom.uuid
yield(e) if block_given?
end
end
def din_platform opts={}
Platform.create!(opts) do |p|
p.primary_engineer_id = din_engineer.id
yield(p) if block_given?
end
end
That will cover 90% of use cases, and limit your setup to just graph construction in each spec file.
If you must move the values of lets into the creation method bodies, you can decorate the creation methods in spec-local method definitions.
# spec/requests/drawing_issues_notices/create_drawing_issue_notice_spec.rb
let(:some_let_value) { "try not doing this, but if you must." }
let(:pic) { complex_din_engineer }
def complex_din_engineer
din_engineer do |e|
e.email = some_let_value
end
end
Yea, looks like divergent goals. My aim was to layout a set of rules for writing code that does the setup for a request spec; with a primary goal of having specs that you can reason about and extend later.
FactoryGirl usually ends up like cucumber, a layer of indirection that gets in the way.
But as you know, unstructured
letand in-spec method definitions get out of hand and hard to follow if your object graph has a spanning tree longer than ~2.I'll cede
FactoyBoyto you, as your usage of it has prior-art in our codebase. Imma try outFieldHandand see how I like that.