BDD Step-by-Step Example (part 2, refactor)

March 29th, 2009 by admin Leave a reply »

The cool thing about Cucumber is how it allows you to reuse your step definitions. It allows you to effectively create a DSL whereby you can generate new tests with little or no additional coding. As you’re writing step definitions keep this in mind, and look for patterns that you can dry up.

Below is an example of a step definition that has grown over time. These steps support a user administration feature using restful_authentication and aasm_state.

Given /^a user that is pending$/ do
  pending
end

When /^I activate the user$/ do
  pending
end

Then /^the user should be able to log in$/ do
  pending
end

Given /^a user that is active$/ do
  pending
end

When /^I suspend the user$/ do
  pending
end

Then /^the user should be in a suspended state$/ do
  pending
end

Then /^the user should not be able to log in$/ do
  pending
end

Given /^a user that is suspended$/ do
  pending
end

When /^I unsuspend the user$/ do
  pending
end

Then /^the user should be in a pending state$/ do
  pending
end

When /^I delete the user$/ do
  pending
end

Immediately you should recognize that there are some patterns here: given something about the user’s state, and when the admin does something to the state, and whether or not you can log in.

Let’s see if we can’t boil these down these 20 some steps into just 4-5? First lets aggregate the common Given’s, using the power of regular expression we can do something like this:

Given /^a user that is (.+)$/ do
end

Hmm, interesting — this might work, so lets try finishing it up:

Given /^a user that is (.+)$/ do |state|
  @user = User.create!(:login => 'testuser', :email => 'test@test.gov', :password => 'testme', :password_confirmation => 'testme', :state =>state)
end

Odd, the test is failing still. Well if you understand how restful_authentication works with aasm you’ll soon discover that any user created on the back-end starts out in a passive state. This actually is going to take some thought, so lets come back to it.

How about all the “When I” steps? Those are easily grouped together. Since restful_auth uses nice verbs for state transitions we can leverage this in our step definitions:

When /^I (.+) the user$/ do |state|
  @user.send("#{state}!")
end

Along the same line of thinking we can apply this to our “Then the user” steps:

Then /^the user should not be able to log in$/ do
  visit login_path
  fill_in "login", :with => @user.login
  fill_in "password", :with => @password
  click_button "Log In"
  response.should contain("Couldn't log you in")
end

Then /^the user should be able to log in$/ do
  visit login_path
  fill_in "login", :with => @user.login
  fill_in "password", :with => @password
  click_button "Log In"
  response.should contain("Logged in successfully")
end

Then /^the user should be in a (.+) state$/ do |state|
  @user.send("#{state}?")
end

There’s a lot going on in the log-in steps, we take the same exact steps to log in a user, the only change is the outcome we expect. Ok so lets refactor that out into a method, and we get:

Then /^the user should not be able to log in$/ do
  login_using_form("Couldn't log you in")
end

Then /^the user should be able to log in$/ do
  login_using_form("Logged in successfully")
end

def login_using_form(expectation)
  visit login_path
  fill_in "login", :with => @user.login
  fill_in "password", :with => @password
  click_button "Log In"
  response.should contain(expectation)
end

Ok, better, so what about the given a user? Anything we create on the backend will start out in a state of passive (short of changing how restful_auth works). Also not all the verbs we would use in a step definition equate to a valid transition in aasm (you cannot transition to passive or pending directly). Well it takes a little more thought, but in the end you might end up with a method like the one below:

Given /^a user that is (.+)$/ do |state|
  state_hash = { :active => "activate!", :suspended => "suspend!" }
  @password = 'testme'
  @user = User.create!(:login => 'testuser', :email => 'test@test.gov', :password => @password, :password_confirmation => @password)
  ##
  # Restful_Authentication doesn't provide direct state transistions for passive or pending,
  # so we do a little tweaking to our user object to get it into the desired state.
  #
  @user.register! unless state == 'passive'  # new accounts created through backend start out as passive
  unless state == 'pending'                  # accounts when registered become pending
    @user.send(state_hash[state.to_sym])     # pending can transition easily to any state
  end
end

Putting it all together we have a step definition file now of only 5 definitions and one helper method. These steps can support a wide combination of user starting states, user state transitions and expectations around logging in and user state post transition. Let the business folks go hog wild!

Given /^a user that is (.+)$/ do |state|
  state_hash = { :active => "activate!", :suspended => "suspend!" }
  @password = 'testme'
  @user = User.create!(:login => 'testuser', :email => 'test@test.gov', :password => @password, :password_confirmation => @password)
  ##
  # Restful_Authentication doesn't provide direct state transistions for passive or pending,
  # so we do a little tweaking to our user object to get it into the desired state.
  #
  @user.register! unless state == 'passive'  # new accounts created through backend start out as passive
  unless state == 'pending'                  # accounts when registered become pending
    @user.send(state_hash[state.to_sym])     # pending can transition easily to any state
  end
end

When /^I (.+) the user$/ do |state|
  @user.send("#{state}!")
end

Then /^the user should not be able to log in$/ do
  login_using_form("Couldn't log you in")
end

Then /^the user should be able to log in$/ do
  login_using_form("Logged in successfully")
end

Then /^the user should be in a (.+) state$/ do |state|
  @user.send("#{state}?")
end

# step helpers --------------------------------------#

# this method simulates a login
def login_using_form(expectation)
  visit login_path
  fill_in "login", :with => @user.login
  fill_in "password", :with => @password
  click_button "Log In"
  response.should contain(expectation)
end

A real-life feature that these steps support:

Feature:  User Administration

So that I can control access to the application
As an admin
I want to manage users

Scenario: Activate a pending user
  Given a user that is pending
  When I activate the user
  Then the user should be able to log in

Scenario: Suspend a user that is active
  Given a user that is active
  When I suspend the user
  Then the user should not be able to log in

Scenario: Unsuspend a user that is suspended
  Given a user that is suspended
  When I unsuspend the user
  Then the user should be in a pending state
  And the user should not be able to log in

Scenario: Purge a user that is active
  Given a user that is active
  When I delete the user
  Then the user should not be able to log in

Scenario: Purge a user that is pending
  Given a user that is pending
  When I delete the user
  Then the user should not be able to log in

Scenario: Purge a user that is suspended
  Given a user that is suspended
  When I delete the user
  Then the user should not be able to log in
Advertisement

1 comment

  1. Adam Lowe says:

    Thank you for two solid posts. Reading through the Rspec beta book right now and was tripping over Cucumber a bit. This was helpful.

Leave a Reply