For all the new projects that I start with my stakeholders I have been pushing Outside–in software development and Specification by example.
Specification by Example really improves the collaboration between delivery teams and facilitates better engagement with business users. For an in depth look into Specification by Example, you should read Gojko Adzic’s great book Specification by Example: How Successful Teams Deliver the Right Software.
Cucumber is my tool of choice for Spec by Example for my Ruby projects. Read The Cucumber Book: Behaviour-Driven Development for Testers and Developers (Pragmatic Programmers)for a deeper understanding into how to use Cucumber for your projects.
Developers I work with usually ask questions on how to get started with Specification by Example and Outside-in development. As with most things the best way to explain is with an example.
Let’s build a Rails application that pulls new email from a user’s Gmail account, which we will call Fetch-it.
First we need to install Rails, if we do not already have it installed. For this article we will be using 3.2.3.
[~/ProjectsNew] ➔ Rails -v Rails 3.2.3
Setting up our project
To get started we will create our Rails app. It is important to note that since we will be using Cucumber we do not need to install test-unit.
[~/ProjectsNew] ➔ rails new fetch-it --skip-test-unit
create
create README.rdoc
create Rakefile
create config.ru
create .gitignore
create Gemfile
create app
create app/assets/images/rails.png
create app/assets/javascripts/application.js
create app/assets/stylesheets/application.css
create app/controllers/application_controller.rb
create app/helpers/application_helper.rb
create app/mailers
create app/models
create app/views/layouts/application.html.erb
create app/mailers/.gitkeep
create app/models/.gitkeep
create config
create config/routes.rb
create config/application.rb
create config/environment.rb
create config/environments
create config/environments/development.rb
create config/environments/production.rb
create config/environments/test.rb
create config/initializers
create config/initializers/backtrace_silencers.rb
create config/initializers/inflections.rb
create config/initializers/mime_types.rb
create config/initializers/secret_token.rb
create config/initializers/session_store.rb
create config/initializers/wrap_parameters.rb
create config/locales
create config/locales/en.yml
create config/boot.rb
create config/database.yml
create db
create db/seeds.rb
create doc
create doc/README_FOR_APP
create lib
create lib/tasks
create lib/tasks/.gitkeep
create lib/assets
create lib/assets/.gitkeep
create log
create log/.gitkeep
create public
create public/404.html
create public/422.html
create public/500.html
create public/favicon.ico
create public/index.html
create public/robots.txt
create script
create script/rails
create tmp/cache
create tmp/cache/assets
create vendor/assets/javascripts
create vendor/assets/javascripts/.gitkeep
create vendor/assets/stylesheets
create vendor/assets/stylesheets/.gitkeep
create vendor/plugins
create vendor/plugins/.gitkeep
run bundle install
Fetching gem metadata from https://rubygems.org/.........
Using rake (0.9.2.2)
Using i18n (0.6.0)
Installing multi_json (1.3.5)
Using activesupport (3.2.3)
Using builder (3.0.0)
Using activemodel (3.2.3)
Using erubis (2.7.0)
Using journey (1.0.3)
Using rack (1.4.1)
Using rack-cache (1.2)
Using rack-test (0.6.1)
Using hike (1.2.1)
Using tilt (1.3.3)
Using sprockets (2.1.3)
Using actionpack (3.2.3)
Using mime-types (1.18)
Using polyglot (0.3.3)
Using treetop (1.4.10)
Using mail (2.4.4)
Using actionmailer (3.2.3)
Using arel (3.0.2)
Using tzinfo (0.3.33)
Using activerecord (3.2.3)
Using activeresource (3.2.3)
Using bundler (1.1.3)
Installing coffee-script-source (1.3.3)
Installing execjs (1.4.0)
Using coffee-script (2.2.0)
Using rack-ssl (1.3.2)
Installing json (1.7.3) with native extensions
Using rdoc (3.12)
Using thor (0.14.6)
Using railties (3.2.3)
Using coffee-rails (3.2.2)
Using jquery-rails (2.0.2)
Using rails (3.2.3)
Installing sass (3.1.19)
Using sass-rails (3.2.5)
Using sqlite3 (1.3.6)
Using uglifier (1.2.4)
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.
[~/ProjectsNew] ➔
Next we need to update our Gemfile with a few dependencies.
[~/ProjectsNew] ➔ cd ./fetch-it/ [~/ProjectsNew/fetch-it] ➔ sudo nano Gemfile
Add the following to the end of our Gemfile
gem 'haml' gem "mail" group :test do gem 'cucumber-rails' gem 'database_cleaner' gem 'rspec-rails' gem 'factory_girl' end
Then we need to run bundle again to install the additional gems.
[~/ProjectsNew/fetch-it] ➔ bundle install Using rake (0.9.2.2) Using i18n (0.6.0) Using multi_json (1.3.5) Using activesupport (3.2.3) Using builder (3.0.0) Using activemodel (3.2.3) Using erubis (2.7.0) Using journey (1.0.3) Using rack (1.4.1) Using rack-cache (1.2) Using rack-test (0.6.1) Using hike (1.2.1) Using tilt (1.3.3) Using sprockets (2.1.3) Using actionpack (3.2.3) Using mime-types (1.18) Using polyglot (0.3.3) Using treetop (1.4.10) Using mail (2.4.4) Using actionmailer (3.2.3) Using arel (3.0.2) Using tzinfo (0.3.33) Using activerecord (3.2.3) Using activeresource (3.2.3) Using addressable (2.2.7) Using bundler (1.1.3) Using nokogiri (1.5.2) Using ffi (1.0.11) Using childprocess (0.3.2) Using libwebsocket (0.1.3) Using rubyzip (0.9.8) Using selenium-webdriver (2.21.2) Using xpath (0.1.4) Using capybara (1.1.2) Using coffee-script-source (1.3.3) Using execjs (1.4.0) Using coffee-script (2.2.0) Using rack-ssl (1.3.2) Using json (1.7.3) Using rdoc (3.12) Using thor (0.14.6) Using railties (3.2.3) Using coffee-rails (3.2.2) Using diff-lcs (1.1.3) Using gherkin (2.9.3) Using term-ansicolor (1.0.7) Using cucumber (1.1.9) Using cucumber-rails (1.3.0) Using database_cleaner (0.7.2) Using factory_girl (3.2.0) Using haml (3.1.4) Using jquery-rails (2.0.2) Using rails (3.2.3) Using rspec-core (2.9.0) Using rspec-expectations (2.9.1) Using rspec-mocks (2.9.0) Using rspec (2.9.0) Using rspec-rails (2.9.0) Using sass (3.1.19) Using sass-rails (3.2.5) Using sqlite3 (1.3.6) Using uglifier (1.2.4) Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed. [~/ProjectsNew/fetch-it] ➔
Now that we have everything installed we need to generate our required Cucumber directores and files.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rails generate cucumber:install
create config/cucumber.yml
create script/cucumber
chmod script/cucumber
create features/step_definitions
create features/support
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
[~/ProjectsNew/fetch-it (master)⚡] ➔
Now that we have our base project setup let’s create a Git repo then add our project.
[~/ProjectsNew/fetch-it] ➔ git init Initialized empty Git repository in /Users/carlos/ProjectsNew/fetch-it/.git/ [~/ProjectsNew/fetch-it (master)⚡] ➔ git add . [~/ProjectsNew/fetch-it (master)⚡] ➔ git commit -m "Initial" [master (root-commit) 4ce447c] Initial 39 files changed, 1396 insertions(+), 0 deletions(-) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 README.rdoc create mode 100644 Rakefile create mode 100644 app/assets/images/rails.png create mode 100644 app/assets/javascripts/application.js create mode 100644 app/assets/stylesheets/application.css create mode 100644 app/controllers/application_controller.rb create mode 100644 app/helpers/application_helper.rb create mode 100644 app/mailers/.gitkeep create mode 100644 app/models/.gitkeep create mode 100644 app/views/layouts/application.html.erb create mode 100644 config.ru create mode 100644 config/application.rb create mode 100644 config/boot.rb create mode 100644 config/cucumber.yml create mode 100644 config/database.yml create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/initializers/backtrace_silencers.rb create mode 100644 config/initializers/inflections.rb create mode 100644 config/initializers/mime_types.rb create mode 100644 config/initializers/secret_token.rb create mode 100644 config/initializers/session_store.rb create mode 100644 config/initializers/wrap_parameters.rb create mode 100644 config/locales/en.yml create mode 100644 config/routes.rb create mode 100644 db/seeds.rb create mode 100644 doc/README_FOR_APP create mode 100644 features/support/env.rb create mode 100644 lib/assets/.gitkeep create mode 100644 lib/tasks/.gitkeep create mode 100644 lib/tasks/cucumber.rake create mode 100644 log/.gitkeep create mode 100644 public/404.html create mode 100644 public/422.html create mode 100644 public/500.html create mode 100644 public/favicon.ico create mode 100644 public/index.html create mode 100644 public/robots.txt create mode 100755 script/cucumber create mode 100755 script/rails create mode 100644 vendor/assets/javascripts/.gitkeep create mode 100644 vendor/assets/stylesheets/.gitkeep create mode 100644 vendor/plugins/.gitkeep [~/ProjectsNew/fetch-it (master)] ➔
Start with our Spec
To get started with development of our Fetch-it email application we are going to first write our spec. We will call this spec Get Email which should clearly explain what feature we are going to implement.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake db:migrate db:test:prepare [~/ProjectsNew/fetch-it (master)] ➔ sudo nano ./features/get_email.feature
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail
Given the user has a Gmail account
When they visit the index page
Then they will see a list of new email
This simple, clear, and concise spec will allow us to build our application in an iterative Outside-In manner. For the rest of the article we will be following a cadence of running Cucumber to see what step fails, implement our scenarios to pass the step, then repeat until everything passes.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/get_email.feature:6
Undefined step: "the user has a Gmail account" (Cucumber::Undefined)
features/get_email.feature:6:in `Given the user has a Gmail account'
When they visit the index page # features/get_email.feature:7
Undefined step: "they select get email from the menu" (Cucumber::Undefined)
features/get_email.feature:7:in `When they visit the index page'
Then they will see a list of new email # features/get_email.feature:8
Undefined step: "they will see a list of new email" (Cucumber::Undefined)
features/get_email.feature:8:in `Then they will see a list of new email'
1 scenario (1 undefined)
3 steps (3 undefined)
0m0.134s
You can implement step definitions for undefined steps with these snippets:
Given /^the user has a Gmail account$/ do
pending # express the regexp above with the code you wish you had
end
When /^they visit the index page$/ do
pending # express the regexp above with the code you wish you had
end
Then /^they will see a list of new email$/ do
pending # express the regexp above with the code you wish you had
end
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
[~/ProjectsNew/fetch-it (master)⚡] ➔
Why are we seeing that rake aborted error? By default Cucumber is set to strict mode, which is why we are seeing the rake error.
... rake aborted! Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...] Tasks: TOP => cucumber => cucumber:ok (See full trace by running task with --trace)
To remove this error we just need to turn off strict mode. This option is configured in our cucumber.yaml.
[~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./config/cucumber.yaml
<%
rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : ""
rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBE$
std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip"
%>
default: <%= std_opts %> features
wip: --tags @wip:3 --wip features
rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip
We just need to remove the –strict option on for the std_opts, right before the –tags option.
<%
rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : ""
rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBE$
std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --tags ~@wip"
%>
default: <%= std_opts %> features
wip: --tags @wip:3 --wip features
rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip
When we run cucumber again the error should be gone.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/get_email.feature:6
When they visit the index page # features/get_email.feature:7
Then they will see a list of new email # features/get_email.feature:8
1 scenario (1 undefined)
3 steps (3 undefined)
0m0.066s
You can implement step definitions for undefined steps with these snippets:
Given /^the user has a Gmail account$/ do
pending # express the regexp above with the code you wish you had
end
When /^they visit the index page$/ do
pending # express the regexp above with the code you wish you had
end
Then /^they will see a list of new email$/ do
pending # express the regexp above with the code you wish you had
end
[~/ProjectsNew/fetch-it (master)⚡] ➔
We have our feature created, but no steps to implement that feature.
Cucumber points us the right direction by giving us examples of steps. What do we do with those examples?
We need to create a file called ./features/step_definitions/get_email_steps.rb and copy the above step definitions examples into that file.
[~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./features/step_definitions/get_email_steps.rb
Given /^the user has a Gmail account$/ do pending # express the regexp above with the code you wish you had end When /^they visit the index page$/ do pending # express the regexp above with the code you wish you had end Then /^they will see a list of new email$/ do pending # express the regexp above with the code you wish you had end
Then we run cucumber again.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
TODO (Cucumber::Pending)
./features/step_definitions/get_email_steps.rb:2:in `/^the user has a Gmail account$/'
features/get_email.feature:6:in `Given the user has a Gmail account'
When they visit the index page # features/step_definitions/get_email_steps.rb:5
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:9
1 scenario (1 pending)
3 steps (2 skipped, 1 pending)
0m0.071s
[~/ProjectsNew/fetch-it (master)⚡] ➔
You can see that Cucumber is telling us that first first step is pending, let’s implement our first step by creating User and Account instances.
[~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./features/step_definitions/get_email_steps.rb
Given /^the user has a Gmail account$/ do
@user = User.create(:email => 'example@example.com')
account = Account.create account_type: 'gmail',
user_name: 'your_user_name',
password: 'your_password'
@user.accounts << account
@user.save
end
When /^they visit the index page$/ do
pending # express the regexp above with the code you wish you had
end
Then /^they will see a list of new email$/ do
pending # express the regexp above with the code you wish you had
end
Back to Cucumber.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
uninitialized constant User (NameError)
./features/step_definitions/get_email_steps.rb:2:in `/^the user has a Gmail account$/'
features/get_email.feature:6:in `Given the user has a Gmail account'
When they visit the index page # features/step_definitions/get_email_steps.rb:11
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new mail from Gmail
1 scenario (1 failed)
3 steps (1 failed, 2 skipped)
0m0.074s
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
We are getting uninitialized constant User (NameError). This makes sense since we do not have a User model defined in our application. Let’s fix it by generating that User model.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rails g model User email:string
invoke active_record
create db/migrate/20120527212233_create_users.rb
create app/models/user.rb
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake db:migrate db:test:prepare
== CreateUsers: migrating ====================================================
-- create_table(:users)
-> 0.0207s
== CreateUsers: migrated (0.0208s) ===========================================
[~/ProjectsNew/fetch-it (master)⚡] ➔
Then run Cucumber again.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
uninitialized constant Account (NameError)
./features/step_definitions/get_email_steps.rb:4:in `/^the user has a Gmail account$/'
features/get_email.feature:6:in `Given the user has a Gmail account'
When they visit the index page # features/step_definitions/get_email_steps.rb:11
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new mail from Gmail
1 scenario (1 failed)
3 steps (1 failed, 2 skipped)
0m0.328s
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
[~/ProjectsNew/fetch-it (master)⚡] ➔
We fixed the User NameError, but now we are getting the same error with Account. Let’s generate our Account model.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rails g model Account user_id:integer account_type:string user_name:string password:string
invoke active_record
create db/migrate/20120527213207_create_accounts.rb
create app/models/account.rb
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake db:migrate db:test:prepare
== CreateAccounts: migrating =================================================
-- create_table(:accounts)
-> 0.0078s
== CreateAccounts: migrated (0.0079s) ========================================
[~/ProjectsNew/fetch-it (master)⚡] ➔
Once again we run cucumber
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
undefined method `accounts' for # (NoMethodError)
./features/step_definitions/get_email_steps.rb:7:in `/^the user has a Gmail account$/'
features/get_email.feature:6:in `Given the user has a Gmail account'
When they visit the index page # features/step_definitions/get_email_steps.rb:11
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new mail from Gmail
1 scenario (1 failed)
3 steps (1 failed, 2 skipped)
0m0.158s
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
[~/ProjectsNew/fetch-it (master)⚡] ➔
We have User and Account models, but they do not know about each other. To fix our next error we need to link our User model to the Account model.
[~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./app/models/user.rb
First we need to add has_many :accounts relationship to our User class.
class User < ActiveRecord::Base attr_accessible :email has_many :accounts end
Then belongs_to :user on our Account class.
[~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./app/models/account.rb
class Account < ActiveRecord::Base attr_accessible :account_type, :password, :user_id, :user_name belongs_to :user end
Now if we run cucumber again, we should have completed our first step.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:11
TODO (Cucumber::Pending)
./features/step_definitions/get_email_steps.rb:12:in `/^they visit the index page$/'
features/get_email.feature:7:in `When they visit the index page'
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
1 scenario (1 pending)
3 steps (1 skipped, 1 pending, 1 passed)
0m0.454s
[~/ProjectsNew/fetch-it (master)⚡] ➔
To implement our next step we need to define our controller actions that will invoke our new models.
[~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./features/step_definitions/get_email_steps.rb
Given /^the user has a Gmail account$/ do
@user = User.create(:email => 'example@example.com')
account = Account.create account_type: 'gmail',
user_name: 'your_user_name',
password: 'your_password'
@user.accounts << account
@user.save
end
When /^they visit the index page$/ do
visit(emails_path)
end
Then /^they will see a list of new email$/ do
pending # express the regexp above with the code you wish you had
end
For When they visit the index page step we use Capybara’s visit method to navigate to our emails_path.
Now we see that we have no route defined for emails_path.
~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:11
undefined local variable or method `emails_path' for # (NameError)
./features/step_definitions/get_email_steps.rb:12:in `/^they visit the index page$/'
features/get_email.feature:7:in `When they visit the index page'
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new mail from Gmail
1 scenario (1 failed)
3 steps (1 failed, 1 skipped, 1 passed)
0m0.452s
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
[~/ProjectsNew/fetch-it (master)⚡] ➔
Let’s fix things by creating our controller and setting up our routes.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rails g controller Emails
create app/controllers/emails_controller.rb
invoke erb
create app/views/emails
invoke helper
create app/helpers/emails_helper.rb
invoke assets
invoke coffee
create app/assets/javascripts/emails.js.coffee
invoke scss
create app/assets/stylesheets/emails.css.scss
[~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./config/routes.rb
FetchIt::Application.routes.draw do resources :emails end
Now when we run rake again we should be closer, but we now need to define our controller actions.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:11
The action 'index' could not be found for EmailsController (AbstractController::ActionNotFound)
./features/step_definitions/get_email_steps.rb:12:in `/^they visit the index page$/'
features/get_email.feature:7:in `When they visit the index page'
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new mail from Gmail
1 scenario (1 failed)
3 steps (1 failed, 1 skipped, 1 passed)
0m0.637s
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
[~/ProjectsNew/fetch-it (master)⚡] ➔
Let’s create our index action.
class EmailsController < ApplicationController
def index
end
end
Again Cucumber
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:11
Missing template emails/index, application/index with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :coffee, :haml]}. Searched in:
* "/Users/carlos/ProjectsNew/fetch-it/app/views"
(ActionView::MissingTemplate)
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/lib/ruby/1.9.1/benchmark.rb:295:in `realtime'
./features/step_definitions/get_email_steps.rb:12:in `/^they visit the index page$/'
features/get_email.feature:7:in `When they visit the index page'
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new mail from Gmail
1 scenario (1 failed)
3 steps (1 failed, 1 skipped, 1 passed)
0m0.229s
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
[~/ProjectsNew/fetch-it (master)⚡] ➔
We are moving up the stack, but Cucumber is now telling us that we are Missing template emails/index. We can easily fix that by creating our index view.
[~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./app/views/emails/index.html.haml
%input{:id => "email_count", :type => "hidden", :value => "#{@emails.count}"}
%h2 You have #{@emails.count} new emails.
%ol
- @emails.each do |email|
%li
= email.subject
Since we are using haml for our markup instead of erb, let’s rename our ./layouts/application.html.erb
[~/ProjectsNew/kanban-mail (master)⚡] ➔ mv ./app/views/layouts/application.html.erb ./app/views/layouts/application.html.haml
Then update with the following:
!!! Strict
%html
%head
%title Fetch-it
= stylesheet_link_tag "application", :media => "all"
= javascript_include_tag "application"
= csrf_meta_tags
%body
%p{:class => "notice"}=notice
%p{:class => "alert"}=alert
=yield
Let’s see what cucumber says now.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:11
undefined method `count' for nil:NilClass (ActionView::Template::Error)
...
./features/step_definitions/get_email_steps.rb:12:in `/^they visit the index page$/'
features/get_email.feature:7:in `When they visit the index page'
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new mail from Gmail
1 scenario (1 failed)
3 steps (1 failed, 1 skipped, 1 passed)
0m0.276s
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
[~/ProjectsNew/fetch-it (master)⚡] ➔
We have our route, controller, and views wired up, but we have no data (models) being passed from our controller.
Let’s update our EmailsController to pass the model that the view is expecting.
[~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./app/controllers/emails_controller.rb
class EmailsController < ApplicationController
respond_to :html
def index
@user = User.find User.first
Email.load_mail @user
@emails = Email.find :all
respond_with emails
end
end
Cucumber
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:11
uninitialized constant EmailsController::Email (NameError)
./app/controllers/emails_controller.rb:6:in `index'
./features/step_definitions/get_email_steps.rb:12:in `/^they visit the index page$/'
features/get_email.feature:7:in `When they visit the index page'
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new mail from Gmail
1 scenario (1 failed)
3 steps (1 failed, 1 skipped, 1 passed)
0m0.196s
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
[~/ProjectsNew/fetch-it (master)⚡] ➔
So now we need to create our Email model.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rails g model Email user_id:integer subject:string
invoke active_record
create db/migrate/20120528023430_create_emails.rb
create app/models/email.rb
~/ProjectsNew/fetch-it (master)⚡] ➔ rake db:migrate db:test:prepare
== CreateEmails: migrating ===================================================
-- create_table(:emails)
-> 0.0078s
== CreateEmails: migrated (0.0079s) ==========================================
[~/ProjectsNew/fetch-it (master)⚡] ➔
Then we need to link our Email model to our User.
class Email < ActiveRecord::Base attr_accessible :subject, :user_id belongs_to :user end
Now let’s create the method that will handle loading our Email model, Email#load_mail
class Email < ActiveRecord::Base
attr_accessible :subject, :user_id
belongs_to :user
class << self
def load_mail user, klass = ::Gmail::Client
account = user.accounts.find_by_account_type('gmail')
klass.fetch account do |mail|
create_params = {
:subject => mail[:subject]
}
create create_params
end
end
end
end
Cucumber tells us that we do not have a Gmail library
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:11
uninitialized constant Gmail (NameError)
./app/models/email.rb:6:in `load_mail'
./app/controllers/emails_controller.rb:6:in `index'
./features/step_definitions/get_email_steps.rb:12:in `/^they visit the index page$/'
features/get_email.feature:7:in `When they visit the index page'
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new mail from Gmail
1 scenario (1 failed)
3 steps (1 failed, 1 skipped, 1 passed)
0m0.202s
rake aborted!
Command failed with status (1): [/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bi...]
Tasks: TOP => cucumber => cucumber:ok
(See full trace by running task with --trace)
[~/ProjectsNew/fetch-it (master)⚡] ➔
Let’s fix that by creating our Gmail::Client library.
[~/ProjectsNew/fetch-it (master)⚡] ➔ mkdir ./lib/gmail [~/ProjectsNew/fetch-it (master)⚡] ➔ sudo nano ./lib/gmail/client.rb
require 'mail'
require 'openssl'
module Gmail
class Client
class << self
def fetch account
Mail.defaults do
retriever_method :imap,
:address => 'imap.gmail.com',
:port => 993,
:user_name => "#{account[:user_name]}@gmail.com",
:password => account[:password],
:enable_ssl => true
end
recent_mail = Mail.find :what => :last,
:keys => ["ALL", "UNSEEN"],
:ready_only => true,
:count => 9999,
:order => :desc
recent_mail.each do |mail|
item = Hash.new
item[:sent] = mail.date
item[:from] = (mail.from || []).join(',')
item[:to] = (mail.to || []).join(',')
item[:cc] = (mail.cc || []).join(',')
item[:bcc] = (mail.bcc || []).join(',')
item[:subject] = mail.subject.to_s
item[:body] = mail.body.to_s.force_encoding('UTF-8')
yield(item) if block_given?
end
end
end
end
end
We need to tell Rails to load our new library. We do this by adding the following to ./config/application.rb
...
module FetchIt
class Application < Rails::Application
...
# Autoload lib
config.autoload_paths += %W(#{Rails.root}/lib)
end
end
Cucumber..
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:11
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
TODO (Cucumber::Pending)
./features/step_definitions/get_email_steps.rb:16:in `/^they will see a list of new email$/'
features/get_email.feature:8:in `Then they will see a list of new email'
1 scenario (1 pending)
3 steps (1 pending, 2 passed)
0m6.753s
[~/ProjectsNew/fetch-it (master)⚡] ➔
Now we will implement our last step of our feature.
Then /^they will see a list of new email$/ do
page.find('#email_count').value.to_i.should be > 0
end
Now when we run Cucumber for the last time, all of our steps are passing.
[~/ProjectsNew/fetch-it (master)] ➔ rake cucumber
/Users/carlos/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Get email
In order to manage email a user should be able
get email from their Gmail account
Scenario: Get new mail from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:11
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:15
1 scenario (1 passed)
3 steps (3 passed)
0m12.389s
[~/ProjectsNew/fetch-it (master)⚡] ➔
Up to this point we have not even looked at our application in the browser. Since we worked from the outside-in we can be confident that the application should behave as specified by our spec.
Our steps create our example data in the test database, so before we run our application we need to add data to our development database. For this we will use the built Rails ./db/seeds.rb file.
@user = User.create(:email => 'example@example.com')
account = Account.create account_type: 'gmail',
user_name: 'your_user_name',
password: 'your_password'
@user.accounts << account
@user.save
To load the data we run the following:
[~/ProjectsNew/fetch-it (master)⚡] ➔ rake db:seed
Now we can fire up the web server and check out our app.
[~/ProjectsNew/fetch-it (master)⚡] ➔ rails s => Booting WEBrick => Rails 3.2.3 application starting in development on http://0.0.0.0:3000 => Call with -d to detach => Ctrl-C to shutdown server [2012-05-27 21:50:13] INFO WEBrick 1.3.1 [2012-05-27 21:50:13] INFO ruby 1.9.3 (2011-10-30) [x86_64-darwin11.3.0] [2012-05-27 21:50:13] INFO WEBrick::HTTPServer#start: pid=34614 port=3000
If we open our browser to http://0.0.0.0:3000/emails We should see something that looks like the following:

To finish things up let’s commit everything to git.
[~/ProjectsNew/fetch-it (master)⚡] ➔ git add . [~/ProjectsNew/fetch-it (master)⚡] ➔ git commit -m "Get Email Feature" [master 3423e4f] Get Email Feature 21 files changed, 233 insertions(+), 327 deletions(-) rewrite README.rdoc (99%) create mode 100644 app/assets/javascripts/emails.js.coffee create mode 100644 app/assets/stylesheets/emails.css.scss create mode 100644 app/controllers/emails_controller.rb create mode 100644 app/helpers/emails_helper.rb create mode 100644 app/models/account.rb create mode 100644 app/models/email.rb create mode 100644 app/models/user.rb create mode 100644 app/views/emails/index.html.haml create mode 100644 app/views/layouts/application.html.haml rewrite config/routes.rb (97%) create mode 100644 db/migrate/20120527212233_create_users.rb create mode 100644 db/migrate/20120527213207_create_accounts.rb create mode 100644 db/migrate/20120528023430_create_emails.rb create mode 100644 db/schema.rb create mode 100644 features/get_email.feature create mode 100644 features/step_definitions/get_email_steps.rb create mode 100644 lib/gmail/client.rb [~/ProjectsNew/fetch-it (master)⚡] ➔
What’s next
We covered a lot while building this application, but there are still a lot more we can do. What are some things we should do next?
- Marking emails that we download as read, right now every time we go to http://0.0.0.0:3000/emails the application will download and load the database with the same sets of emails. Essentially creating duplicate records in the database.
- Making the email list items clickable links that take you to the email detail.
- Adding an additional scenario to our ./features/get_email.feature for cases when there is no email on the server
- Other ideas?
The code
If you want to get the full application we built you can clone the repo on GitHub
[~/ProjectsNew] ➔ git clone https://CarlosGabaldon@github.com/CarlosGabaldon/fetch-it.git
The second step fails for me:
Scenario: Get new email from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:12
undefined method `join’ for “\”\” “:String (NoMethodError)
./lib/gmail/client.rb:28:in `block in fetch’
./lib/gmail/client.rb:23:in `each’
./lib/gmail/client.rb:23:in `fetch’
./app/models/email.rb:9:in `load_mail’
./app/controllers/emails_controller.rb:5:in `index’
./features/step_definitions/get_email_steps.rb:13:in `/^they visit the index page$/’
features/get_email.feature:7:in `When they visit the index page’
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:16
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new email from Gmail
I think the ‘join’ failed when I had hundreds of unread Gmail messages. When I ran cucumber the next time it failed at a different place:
Scenario: Get new email from Gmail # features/get_email.feature:5
Given the user has a Gmail account # features/step_definitions/get_email_steps.rb:1
When they visit the index page # features/step_definitions/get_email_steps.rb:12
undefined local variable or method `emails’ for # (NameError)
./app/controllers/emails_controller.rb:7:in `index’
./features/step_definitions/get_email_steps.rb:13:in `/^they visit the index page$/’
features/get_email.feature:7:in `When they visit the index page’
Then they will see a list of new email # features/step_definitions/get_email_steps.rb:16
Failing Scenarios:
cucumber features/get_email.feature:5 # Scenario: Get new email from Gmail
1 scenario (1 failed)
3 steps (1 failed, 1 skipped, 1 passed)
0m1.643s
rake aborted!
Command failed with status (1): [/usr/local/bin/ruby -S bundle exec cucumbe…]
I figured it out. It should be respond_with(@emails). I am using Rails 3.1.0 so maybe the syntax you have used works with later versions?