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
let
creation 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!
yield
back to the caller.- You are not allowed to put any
let
s 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
let
s 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 }
end
Becomes:
# 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
I'm not sure if we're trying to solve all the same problems, but the ideal pattern in my mind is:
If there's more than a couple of these, the duplication would be annoying. At that point I'd say install FactoryGirl.