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.
Steve Klabnik
Sep 10, 2012
How you can include SharedPaths, SharedForms, etc. when your modules is called SharedPath, SharedForm?
I’ll be happy to inspect it closer if you put code to github, tho.
Steve Klabnik
Sep 10, 2012
* are
narkoz
Sep 10, 2012
Recently we at GitLab switched from Cucumber to Spinach, too.
Here is a summary: https://github.com/gitlabhq/gitlabhq/pull/1426
We are more than happy with the transition.
Dan Galipo
Sep 11, 2012
Nice catch Steve :) I have updated the post to fix that typo.
Aaron
Sep 13, 2012
I haven’t checked out Spinach that much but I do like to use World a lot in Cucumber for sharing modules. For instance we use an autocomplete widget let’s say widgetThingA and so I wrote a module that would allow you do interact with it. The gross logic sat in that module.
When we switched to widgetThingB lately I changed it in one place and no steps changed. It’s responsibility principle baby!
As for your issue with Cucumber sharing stuff, I think you just end up doing some sniffing inside your steps and then making calls out to the appropriate modules.
In our case though we don’t have very many ‘fill in form’ type steps, they’re more ‘create a thing’ which means you end up with very little overlap. Creating ThingA and ThingB are typically different.
Have you tried writing more declarative steps? I’ve found way less overlap between steps and it tends to be much cleaner overall. If this won’t work then I’d lean toward steps that do the appropriate context discovery and then inject/utilise whichever module you wish.
Nice one though! :)