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 21, 2014

I'm not sure if we're trying to solve all the same problems, but the ideal pattern in my mind is:

module FactoryBoy
  module_function
  def create_model attributes={}
    build_model(attributes).save!
  end
  def build_model attributes={}
    Model.new model_attributes(attributes)
  end
  def model_attributes attributes={}
    { required: "values", in: "here" }.merge attributes
  end
end
describe FactoryBoy do
  subject { described_class }
  its(:create_model)     { is_expected.to be_persisted }
  its(:build_model)      { is_expected.to be_valid }
  its(:model_attributes) { is_expected.to be_a(Hash) }
end

If there's more than a couple of these, the duplication would be annoying. At that point I'd say install FactoryGirl.

@markjlorenz
Copy link
Author

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 let and 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 FactoyBoy to you, as your usage of it has prior-art in our codebase. Imma try out FieldHand and see how I like that.

@mikegee
Copy link

mikegee commented Jun 23, 2014

Your spec/support/drawing_issue_notices.rb isn't wrapped in module SpecSupportDrawingIssueNotices.

@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