• Text Post

The training wheels came off

Cucumber rounded the 1,000,000 download mark a couple of days ago, and is clearly a very popular tool. It owes a lot of its popularity to Cucumber-Rails - a Ruby gem that sets up Cucumber in a Rails project. One of the reasons Cucumber-Rails has become popular is that it is relatively easy to get started with.

Most people who learned to ride a bike as a child had training wheels. When a child learns to ride on his or her own, the training wheels are removed. Until last week, Cucumber-Rails also came with pre-mounted training wheels, in the form of a generated web_steps.rb file with 30 or so reusable step definitions.

This gave a lot of people a flying start with Cucumber and Rails. Many people never removed the training wheels. Instead they soldered them to the frame by piling up with long, verbose and brittle scenarios that depend on them.

Half a year ago we added a warning to the generated web_steps.rb, because relying on them has so many negative effects.

Some people followed this advice, but there are still a lot of people who fall into the trap. In Cucumber-Rails 1.1.0 they were removed for good, and in 1.1.1 we also removed its tricycle cousin - the cucumber:feature generator. The training wheels came off.

The training wheels came off

The reactions were exactly as I had expected. -A combination of applause and gnashing of teeth. Most of the people who disagree say something along the lines of:

This is going to make it harder for people to get started with Cucumber.

I say: this is going to make it harder for people to use Cucumber badly. Now they have to:

  1. Learn the Capybara API instead of the web_steps.rb regular expressions.
  2. Think harder about what goes into a scenario.

If this hurts the adoption of Cucumber-Rails I’m perfectly fine with it. You already have plenty of rope to hang yourself with, and I’m not going to tie the noose for you.

Let me explain why web_steps.rb is a terrible, terrible idea.

Boring scenarios

Cucumber was designed around the core principles of BDD, and one of these principles is to improve stakeholder collaboration through a ubiquitous language. This essentially means that both code and executable specifications (Cucumber scenarios) should be written in the language of the domain. The “domain” is defined by the value the stakeholders and users hope to achieve with the software. This can be booking a ticket or sharing pictures with friends or an infinite number of activities. Clicking links and buttons or filling in text fields has nothing to do with the domain.

Cucumber scenarios that consist of 10 or so steps that click links, fill in fields, push buttons and look for text are going to bore your stakeholders to death.

Their eyes will glaze over, and they won’t even spot mistakes. Even programmers will have a hard time spotting conceptual errors at this abstraction level. Using Cucumber exclusively to emulate a keyboard and a mouse is possible, but it’s not what it was designed for.

If all you need is a testing tool for driving a mouse and a keyboard, don’t use Cucumber. There are other tools that are designed to do this with far less abstraction and typing overhead than Cucumber. Capybara DSL and Steak are much better if you want a programmer/tester-only tool.

Brittle scenarios

Allowing technical details to bleed into scenarios does more harm than losing stakeholder involvement through boredom. It makes everything harder to change as well. Consider this scenario for logging in:

Scenario: Successful login
  Given a user "Aslak" with password "xyz"
  And I am on the login page
  And I fill in "User name" with "Aslak"
  And I fill in "Password" with "xyz"
  When I press "Log in"
  Then I should see "Welcome, Aslak"

If users have to log in there is usually a lot of functionality that is only accessible to users who have logged in. This means logging in has to happen before most scenarios:

Background: A logged in user
  Given a user "Aslak" with password "xyz"
  And I am on the login page
  And I fill in "User name" with "Aslak"
  And I fill in "Password" with "xyz"
  When I press "Log in"
  Then I should see "Welcome, Aslak"

You’ll have to repeat this in all of your scenarios that describe functionality for logged in users. When someone decides to allow users to log in with an email address instead of a user name we have to go over all of our scenarios and change them.

It’s not a question of if the user interface has to change, but when. Usually a UI change has far wider consequences than this example.

Where is my workflow?

Cucumber was designed to help developers with TDD at a higher level. A common challenge with conventional TDD is that developers don’t know where to start. It’s hard to figure out what tests to write.

The idea with Cucumber (and BDD in general) is that stakeholders assist in writing scenarios - or executable specifications. This solves the where do we start problem with TDD. The scenarios express what a user should be able to do, and not how. When a scenario is defined, programmers implement the required functionality.

This kind of workflow is much harder to follow when scenarios are written in a low-level, imperative style. Very few stakeholders or business analysts are going to agree to defining functionality in terms of mouse clicks and key presses. They think and talk at a higher abstraction level, and scenarios should capture that.

I don’t care about this tree hugging stuff, give me my web_steps.rb

When I started the Cucumber project in 2008 I had three major goals:

  • Help stakeholders express what they want via executable specifications
  • Help programmers write better software by making TDD/BDD easy
  • Reduce misinterpretations through a ubiquitous language

Scenarios based on web_steps.rb undermine those goals, and that is why I decided to remove them. Maybe it’s a pride thing. -Like the shopkeeper who refuses to sell merchandise he believes is of bad quality, even if doing so would mean more customers.

Unfortunately, there are books, screencasts and tutorials out there that teach you Cucumber using web_steps.rb. If web_steps.rb disappeared for good this would harm the people who are selling this training material. People who have bought it won’t get what they paid for. So I present to you: cucumber-rails-training-wheels.

This is a stillborn project. I will not accept bug reports or pull requests. The only reason it exists is to make old tutorials and books work.

Fine. I still think it’s easier to write scenarios based on web_steps.rb

Which one of these lines are easier to write?

When I press "Log in"
click_button("Log in")

I don’t think it’s harder to read the Capybara API than the regular expressions in web_steps.rb. If it is, someone needs to improve the Capybara API docs.

Can you give me an example of the new way?

Let’s take the login scenario above and make it a little more readable:

Scenario: User is greeted upon login
  Given the user "Aslak" has an account
  When he logs in
  Then he should see "Welcome, Aslak"

The step definition for the first step would create a new User record and store a reference in a @user variable. This would use ActiveRecord directly. The interesting part is logging in. Cucumber prints out snippets of code for undefined steps:

When /^he logs in$/ do
  # express the regexp above with the code you wish you had
end

Assuming we know what the log in screen is supposed to look like, we can implement this step definition:

When /^he logs in$/ do
  visit('/login')
  fill_in('User name', :with => @user.name)
  fill_in('Password', :with => @user.password)
  click_button('Log in')
end

That wasn’t so hard. Now let’s improve on that Background we had earlier so we can log in before other scenarios:

Background: The user is logged in
  Given a logged in user

Scenario: Upload a picture
  # Some steps here

When we’re describing other parts of the system that require login, the login details like user name, password and how to log in are only distracting. That is why we make it a one-liner. Let’s look at how we would define that:

Given /^a logged in user$/ do
  @user = User.create!(:name => 'Aslak', :password => 'xyz')
  visit('/login')
  fill_in('User name', :with => @user.name)
  fill_in('Password', :with => @user.password)
  click_button('Log in')
end

Can you spot the duplication with the When /^he logs in$/ step definition? Let’s improve this:

module LoginSteps
  def login(name, password)
    visit('/login')
    fill_in('User name', :with => name)
    fill_in('Password', :with => password)
    click_button('Log in')
  end
end

World(LoginSteps)

Now your two step definitions can be simplified:

When /^he logs in$/ do
  login(@user.name, @user.password)
end

Given /^a logged in user$/ do
  @user = User.create!(:name => 'Aslak', :password => 'xyz')
  login(@user.name, @user.password)
end

Not only have you made your Scenario and Background easier to read, you also isolated the login details in one single place. Changing the login process to use an email instead of a name will be easy. No scenarios have to change, and you only have to change a couple of places in your step definitions.

Conclusion

Removing the web_steps.rb training wheels makes Cucumber harder to use badly. You have to learn the Capybara API instead of regular expressions, and you will take more advantage of the snippets Cucumber prints for undefined steps. This also means you have to think in terms of the domain and not the user interface when you write scenarios. This makes them a lot easier to maintain. Short, declarative scenarios serve as easily readable documentation, and non-technical stakeholders and business analysts are more likely to get intimate with them.

There are also a couple of things you no longer have to do:

  • Wade through web_steps.rb and try to make your steps fit their mold. (You’re in charge and can say what you want)
  • Update a cryptic paths.rb or selectors.rb file
  • Be afraid to change generated code you don’t really understand

Here are some great resources for more information:

Happy cuking!

Notes

  1. fireiceriver reblogged this from aslakhellesoy

Comments

blog comments powered by Disqus