Skip to content

Instantly share code, notes, and snippets.

@markjlorenz
Last active August 29, 2015 14:02
Show Gist options
  • Save markjlorenz/17b8a153286752134ee7 to your computer and use it in GitHub Desktop.
Save markjlorenz/17b8a153286752134ee7 to your computer and use it in GitHub Desktop.
Explaining the FieldHand technique.

Introducing FieldHand!

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:

  1. Duplicate the setup, and linking of multple objects across different files.
  2. Have very long test files to avoid duplicate setup ( a.k.a fixing (1) the lazy way )
  3. Extracting let creation into a module. This may seem like a good idea at first, but it quickly becomes very hard to override the right let, and even harder to figure out what your test is doing.

A Solution: Follow these simple rules:

  1. Extract model creation to methods in a module
  2. You are allowed to supply default attributes to create!
  3. yield back to the caller.
  4. You are not allowed to put any lets in the module
  5. In the spec, call your extracted methods
  6. In the spec, if you need a "sticky" instance, wrap a call to your extracted method in a let
  7. 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 }
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
@mikegee
Copy link

mikegee commented Jun 23, 2014

def din_platform opts={}
  Platform.create!(opts) do |p|
    p.primary_engineer_id = din_engineer.id
    yield(p) if block_given?
  end
end

What if I have a primary_engineer handy? I can overwrite primary_engineer_id in the block, but another engineer was needlessly created.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment