Recently, I switched over from using Cucumber to Spinach in my Rails and mobile projects. I have been enjoying using Spinach so far and thought I’d share a pattern I’ve been using that seems to work for me.

Now I would like to preface this by saying that:

  • I’m not an expert on the topic
  • I’m not saying Spinach is better than Cucumber (or vice versa)

One of the things I found myself doing often when I was naming Cucumber steps was writing names that became very convoluted. I did this because Cucumber scopes all steps globally and naming two steps identically will cause the feature to fail with “Ambiguous match of “step name””. This meant I had to name almost identical steps slightly differently which would get compounded the more similar steps I had.

For example:

And I fill out the object create form with valid details
And I fill out the object create form with valid details using the google auto complete
And I fill out the object create form with valid details using the google auto complete and marking it as draft

When what I really wanted to say was:

And I fill out the form with valid details

All of that other information was covered in the feature name and/or scenario description.

When I moved to Spinach I found that I wasn’t having that problem because each feature’s steps were available only to that feature. I thought this was great… until I found myself defining identical steps in 20 different files.

My friend Tony Issakov then mentioned that you could define modules, include the Spinach DSL and share them between features. I was also warned, by my friend Jordan Maguire, that if you do this too much, you can end up with step definition files that are just includes and aren’t very readable.

For Example:

Feature: Updating an existing object

Background:
  Given I am logged in as an admin
  And there is an object in the system
  And I am on the object index page

Scenario: Successfully updating object
  When I click the object's name
  And I fill out the form with valid details
  Then I should be be on the object's show page
  And I should see the object's details have been update

Scenario: Failed to update object
  When I click the object's name
  And I fill out the form with invalid details
  Then I should see the form again
  And I should see any form errors
class UpdatingAnExistingObject < Spinach::FeatureSteps
  include SharedLinks
  include SharedPath
  include SharedForm
  include SharedObject
  include SharedViews
end

While this is an extreme example you can see that it quickly becomes difficult to know what step is defined where.

After a bit of playing around I found a happy balance between too much and too little step sharing. My general rule of thumb is to only share the following types of steps:

Authentication

module SharedAuthentication

  include Spinach::DSL

  Given 'I am logged in as an admin' do
    visit new_user_session_path
    user = FactoryGirl.create :admin
    login_with user.email, 'secret'
  end

  def login_with email, password
    fill_in 'user[email]', with: email
    fill_in 'user[password]', with: password
    click_button :submit
  end

end

Paths

module SharedPath

  include Spinach::DSL

  And 'I am on the objects index page' do
    visit objects_path
  end

  And "I am on the object's show page for my venue" do
    current_path.should == object_path(@object)
  end

  Then 'I should be on the objects index page' do
    current_path.should == objects_path
  end

end

Forms

Note that I don’t have any of the “And I fill in the form” steps in here. This is because I want separate them out and just have ‘And I fill out the form with valid details’ in my step definition.

module SharedForm

  include Spinach::DSL

  And 'I should see any form errors' do
    page.should have_css 'p.inline-errors'
  end

  def fill_in_object_edit_form attributes = nil
    if attributes.present?
      fill_in 'object[name]', with: attributes[:name]
      # fill in other attributes
    else
      fill_in 'object[name]', with: ''
    end
    click_button :submit
  end
end

Flash

module SharedFlash

  include Spinach::DSL

  And 'I should see a confirmation message' do
    page.should have_css 'p.notice'
  end

  And 'I should see a flash alert' do
    page.should have_css 'p.alert'
  end

end

Object creation

I generally like to have one per model that handles basic object creation.

module SharedObject

  include Spinach::DSL

  And 'there is object in the system' do
    @object = FactoryGirl.create :object
  end

  ## Where mk_two_object is another factory for Object
  And 'there is a mark two object in the system' do
    @mk_two_object = FactoryGirl.create :mk_two_object
  end

end

If we put it all together using the above example we get something that looks like this:
For Example:

Feature: Updating an existing object

Background:
  Given I am logged in as an admin
  And there is an object in the system
  And I am on the object index page

Scenario: Successfully updating object
  When I click the object's name
  And I fill out the form with valid details
  Then I should be be on the object's show page
  And I should see the details have been update

Scenario: Failed to update object
  When I click the object's name
  And I fill out the form with invalid details
  Then I should see the form again
  And I should see any form errors
class UpdatingAnExistingObject < Spinach::FeatureSteps
  include SharedPath
  include SharedForm
  include SharedObject
  include SharedAuthentication

  When "I click the object's name" do
    click_link @object.name
  end

  And 'I fill out the form with valid details' do
    @attributes = FactoryGirl.attributes_for :object
    fill_in_object_edit_form @attributes
  end

  And 'I should see the details have been update' do
    page.should have_content @attributes[:name]
    ## ...
  end

  And 'I fill out the form with invalid details' do
    fill_in_object_edit_form
  end

  Then 'I should see the form again' do
    page.should have_css "form#edit_object_#{@object.id}"
  end
end

Using this pattern I’ve found that I get the step separation that I was looking for in Spinach while keeping the shared functionality that I enjoyed in Cucumber.

I hope other people find this useful.