Building a Rails Gmail Client Outside-In

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

5 thoughts on “Building a Rails Gmail Client Outside-In

  1. 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

  2. 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…]

Leave a comment