Cucumber is a great weapon in the arsenal of any web developer.
Unfortunately, in mid-November 2010 (when this material was first written), most of the documentation for setting up Cucumber with Rails is for Rails 2 instead of Rails 3.
This article helps fix a little bit of that.
Best Practice in Cucumber has evolved considerably since this article was first written.
Just to be clear, the purpose of the exercise here is:
- Demonstrate what the Outside-In cycle looks like
- Demonstrate approximately how test-first is implemented
Again: nothing in this presentation should be construed as a BDD Best Practice.
This article and code is a from-scratch re-implementation of Sarah Mei's Outside In BDD: How? updated for Rails 3.
But there are some differences. Here, we start with a bare Rails application. The only generators we're going to use are for installing Cucumber. Then, we'll drive the development one file and one method at a time. You will see lots of familiar error messages, along with exactly how those errors were fixed.
Here, we have a user adding a new book title to a list of book titles. That's all the information necessary to build out and test with Cucumber.
I'm using the following setup:
- ruby 1.9.3-p0
rvm use 1.9.3@outsidein
- rails 3.2.1
- Gemfile to follow...
First up, create your new Rails code:
@@@ sh
$ rails new outsidein
$ cd outsidein
$ rm public/index.html
If bundler segfaults, this is most likely a
problem with the openssl
library which it
was compiled against.
For now, change the source argument from https
to
http
in the Gemfile
Add cucumber-rails
, rspec
and database_cleaner
to :development
and :test
groups in your Gemfile:
@@@ ruby
source 'http://rubygems.org'
gem 'rails', '3.2.1'
gem 'sqlite3'
group :assets do
gem 'sass-rails'
gem 'coffee-script'
gem 'uglifier'
end
gem 'jquery-rails'
group :test, :development do
gem 'cucumber-rails'
gem 'database_cleaner'
gem 'rspec'
gem 'spork' #optional
end
As usual, run bundler:
@@@ sh
$ bundle install
Fetching source index for http://rubygems.org/
Using rake (0.9.2)
Using multi_json (1.0.3)
.
.
.
Using turn (0.8.2)
Using uglifier (0.5.4)
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.
$
@@@ sh
$ rails generate cucumber:install
create config/cucumber.yml
create script/cucumber
chmod script/cucumber
create features/step_definitions
create features/step_definitions/web_steps.rb
create features/support
create features/support/paths.rb
create features/support/selectors.rb
create features/support/env.rb
exist lib/tasks
create lib/tasks/cucumber.rake
gsub config/database.yml
gsub config/database.yml
force config/database.yml
$
At this point, we're about ready to write our application.
Let's create our first feature, features/book.feature
:
@@@ gherkin
Feature: User manages books
Scenario: User adds a new book
Given I go to the new book page
And I fill in "Name" with "War & Peace"
When I press "Create"
Then I should be on the book list page
And I should see "War & Peace"
$ cucumber
We have no steps.
Solution: Add file features/step_definitions/book_steps.rb
, and copy in the output:
@@@ ruby
Given /^I go to the new book page$/ do
pending # express the regexp above with the code you wish you had
end
Given /^I fill in "([^"]*)" with "([^"]*)"$/ do |arg1, arg2|
pending # express the regexp above with the code you wish you had
end
When /^I press "([^"]*)"$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
Then /^I should be on the book list page$/ do
pending # express the regexp above with the code you wish you had
end
Then /^I should see "([^"]*)"$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
$ cucumber
@@@ sh
Using the default profile...
Feature: User manages books
Scenario: User adds a new book # features/book.feature:2
Deprecated: please use #source_tags instead.
Given I go to the new book page # features/step_definitions/book_steps.rb:1
TODO (Cucumber::Pending)
./features/step_definitions/book_steps.rb:2:in `/^I go to the new book page$/'
features/book.feature:3:in `Given I go to the new book page'
Solution:
@@@ ruby
Given /^I go to the new book page$/ do
visit new_book_path
end
$ cucumber
We're failing at the first step of the scenario: undefined local variable or method `new_book_path' for #Cucumber::Rails::World:0x00000102ceac68 (NameError).
Solution: Add resources :books
to config/routes.rb
.
While we're at it, go ahead and add a root path, this will be helpful
later: root :to => 'books#index'
.
If you're running Spork, you will need to restart rails to acquire the reconfigured routes.
$ cucumber
Failing again: uninitialized constant BooksController (ActionController::RoutingError)
Solution: Add the controller file app/controllers/books_controller.rb
@@@ ruby
class BooksController < ApplicationController
end
$ cucumber
Failing again: The action 'new' could not be found for
BooksController (AbstractController::ActionNotFound)
Solution: Add the new
method:
@@@ ruby
class BooksController < ApplicationController
def new
end
end
$ cucumber
Missing template books/new with {:handlers=>[:erb, :rjs, :builder, :rhtml, :rxml], :formats=>[:html], :locale=>[:en, :en]} in view paths "/Users/daviddoolin/src/bdd/app/views" (ActionView::MissingTemplate)
@@@ sh
$ mkdir app/views/books
$ vi app/views/books/new.html.erb
Just stick an h1
in that file or something:
@@@ html
<h1>New book page<h1>
$ cucumber
Passed!
One down, four to go.
Cucumber now fails on the second step:
$ cucumber
@@@ sh
And I fill in "Name" with "War & Peace" # features/step_definitions/book_steps.rb:5
TODO (Cucumber::Pending)
./features/step_definitions/book_steps.rb:6:in `/^I fill in "([^"]*)" with "([^"]*)"$/'
features/book.feature:4:in `And I fill in "Name" with "War & Peace"'
Solution: add the Capybara fill_in
matcher:
@@@ ruby
Given /^I fill in "([^"]*)" with "([^"]*)"$/ do |arg1, arg2|
fill_in(arg1, :with => arg2)
end
$ cucumber
Fails: cannot fill in, no text field, text area or password field with id, name, or label 'Name' found (Capybara::ElementNotFound)
Solution: Add a form to the new book page:
@@@ ruby
<%= form_for @book do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.submit 'Create' %>
<% end %>
$ cucumber
Massive FAIL! undefined method `model_name' for NilClass:Class (ActionView::Template::Error)
Solution: Add instance variable to make Rails happy. In app/controllers/books_controller.rb
, add @book = Book.new
, like so:
@@@ ruby
def new
@book = Book.new
end
$ cucumber
Failing on uninitialized constant BooksController::Book (NameError)
.
Solution: This is a somewhat confusing error messge, we need a model to make Rails happy:
@@@ sh
$ vi app/models/book.rb
Make it look like this:
@@@ ruby
class Book < ActiveRecord::Base
end
$ cucumber
Failing Step 1 again... Could not find table 'books' (ActiveRecord::StatementInvalid)
Solution: First, create and edit a migration file:
@@@ sh
$ mkdir db/migrate
$ vi db/migrate/20101120141414_create_books.rb
Then create the migration:
@@@ ruby
class CreateBooks < ActiveRecord::Migration
def self.up
create_table :books do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :books
end
end
And run the migration:
@@@ sh
$ rake db:migrate
$ rake db:test:prepare
Running cucumber again, we pass. Excellent.
$ cucumber
On to our next step:
@@@ sh
When I press "Create" # features/step_definitions/book_steps.rb:9
TODO (Cucumber::Pending)
./features/step_definitions/book_steps.rb:10:in `/^I press "([^"]*)"$/'
features/book.feature:5:in `When I press "Create"'
Solution: add the Capybara click_button
matcher:
@@@ ruby
When /^I press "([^"]*)"$/ do |arg1|
click_button 'Create'
end
$ cucumber
cucumber fails on action 'create': The action 'create' could not be found for BooksController (AbstractController::ActionNotFound)
Solution: Add the create method to the books controller:
@@@ ruby
def create
end
$ cucumber
Failing again on templates: Missing template books/create with {:handlers=>[:erb
Solution: We don't really want a "create" template, so let's go ahead and redirect this to the root_path for now:
@@@ ruby
def create
redirect_to root_path
end
$ cucumber
Failing and failing and failing: The action 'index' could not be found for BooksController
.
Solution: Open app/controllers/books_controller.rb
, add
@@@ ruby
def index
end
$ cucumber
Bummer: Missing template books/index with {:handlers=>[:erb
.
Solution: Add app/views/books/index.html.erb:
@@@ html
<h2>List books</h2>
$ cucumber
Step 3 now passes cucumber. Onward, through the fog.
$ cucumber
@@@ sh
Then I should be on the book list page # features/step_definitions/book_steps.rb:13
TODO (Cucumber::Pending)
./features/step_definitions/book_steps.rb:14:in `/^I should be on the book list page$/'
features/book.feature:6:in `Then I should be on the book list page'
Time to fill in for the next step, this time with a matcher:
@@@ ruby
Then /^I should be on the book list page$/ do
page.should have_content('List books')
end
$ cucumber
And that passes Step 4.
$ cucumber
@@@ sh
And I should see "War & Peace" # features/step_definitions/book_steps.rb:17
TODO (Cucumber::Pending)
./features/step_definitions/book_steps.rb:18:in `/^I should see "([^"]*)"$/'
features/book.feature:7:in `And I should see "War & Peace"'
Time to fill in for the next step, this time with a matcher:
@@@ ruby
Then /^I should see "([^"]*)"$/ do |arg1|
page.should have_content(arg1)
end
$ cucumber
Not seeing books: expected there to be text "War & Peace" in "List books"
.
Solution: Render the book list. First, open the template file:
@@@ sh
$vi app/views/books/index.html.erb
Now render the books:
@@@ ruby
<h2>List books</h2>
<%= render @books %>
$ cucumber
undefined method `model_name' for NilClass:Class (ActionView::Template::Error)
.
Solution: Grab the list of books:
@@@ ruby
def index
@books = Book.all
end
$ cucumber
Still failing... Missing partial books/book with {:handlers=>[:erb,
.
Solution: Add the partial app/views/books/_book.html.erb
@@@ ruby
<%= book.name %>
$ cucumber
expected #has_content?("War & Peace") to return true, got false
Solution: We're almost done, add a little bit of code to the book controller's create
method:
@@@ ruby
def create
@book = Book.new(params[:book])
if @book.save
redirect_to root_path
end
end
Here's what the entire controller class should look like now:
@@@ ruby
class BooksController < ApplicationController
def index
@books = Book.all
end
def new
@book = Book.new
end
def create
@book = Book.new(params[:book])
if @book.save
redirect_to root_path
end
end
end
$ cucumber
We're done.
Notes:
- RSpec only for matchers. In the future (2013?), Capybara matchers may be sufficient.
- All custom step definitions, no
web_steps.rb
matchers.
This isn't the only way to do this. Here are more references on the same topic:
- Outside In Development from Ruby Learning.
- Techiferous gives us Using Capybara in Rails 3.
- John Wyles turns a pickle into a cucumber.
- Francis Fish handles Devise with his cucumber.
If you have an article you believe should be linked, let me know in the comments and I'll add it in.
Overall, this was a lot of work. But there's more which could be done. For example:
The entire project could be rewritten in RSpec alone, save the feature file.
What would you do? Did you give this 5-step procedure whirl? Leave a note in the comments!
Enjoy!