Using BDD and the email_spec gem to implement Email
When implementing email functionality, the email_spec gem is something I’ve decided I can’t live without. It makes it so easy to write RSpec specs and Cucumber features around your email that you have no excuse not to. Today I’m going to go through an example how I recently used BDD to send an email in an app I was working on.
When I think about email and my Rails environments this is how I typically want them to behave.
- test should not send emails and allow us to write specs or features against them
- development should not send emails but provide a UI to view what would be sent on localhost
- staging should not send emails but provide a UI to view what would be sent on a server
- production should send real emails to real people
Last week I wrote about
using letter opener to View Sent Email on a Server (without actually sending anything)
describing how the letter_opener gem
helps us in the development and staging environments.
This article focuses on using email_spec gem
in the test environment.
Our Sample Project
Let’s imagine we are working on a new startup in stealth mode. We want to generate buzz and prepare for a beta launch. We’re hiding the fact its all vaporware with a splashy homepage where people can request an invitation to the beta and records their email in our database. This is the same example as my article last week and there’s a live demo at http://awesome-site-staging.heroku.com/.
What we’ll do today is not just record the email address but also send a “thanks for your interest” email to the user.
We’re going to do this in BDD fashion bouncing back and forth between Cucumber Scenarios
and RSpec Unit Tests
- Writing a cucumber scenario
- While the scenario fails
- Write a failing rspec spec
- Write code to make it pass
The Cucumber scenario tells us what should be accomplished and when it fails it we use that to tell us what unit test we should write.
Adding the email_spec gem
We add it to our Gemfile
group :test do
gem 'email_spec'
end
We bundle and use the email_spec generator to let it initialize itself.
$ bundle
$ rails g email_spec:steps
There’s also a manual step to get cucumber to load the email_spec gem. We need to create a file features/support/email_spec.rb
require 'email_spec/cucumber'
Our first Cucumber Scenario
We know that when a user requests an invitation they should get an email so we write that requirement as a Cucumber Scenario.
features/request_an_invitation.feature
Feature: Build excitement for this vaporware
In order to drum up interest
As a user
I will receive an exciting email when I request an invitation
Scenario: Someone requests an invitation and receives an email
Given I am on the home page
When I request an invitation for "gullible@lemmings.com"
Then "gullible@lemmings.com" should receive 1 email
And they open the email
And they should see the email delivered from "alex@awesome-startup.com"
And they should see "Invitation request for Awesome New Startup received" in the email subject
And they should see "Dear gullible@lemmings.com," in the email text part body
And they should see "We have received your request " in the email text part body
And they should see "Please check back at http://awesome-site-staging.heroku.com" in the email text part body
We run it and it tells us we have a couple of missing steps.
1 scenario (1 undefined)
9 steps (7 skipped, 2 undefined)
0m0.006s
You can implement step definitions for undefined steps with these snippets:
Given /^I am on the home page$/ do
pending # express the regexp above with the code you wish you had
end
When /^I request an invitation for "([^"]*)"$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
Oh right, the training wheels came off in cucumber-rails v1.1.1
and we don’t have web_steps.rb
anymore. Let’s write these steps using capybara in features/step_definitions/invite_steps.rb
Given /^I am on the home page$/ do
visit root_path
end
When /^I request an invitation for "([^"]*)"$/ do |email|
visit root_path
fill_in 'email', :with => email
click_button 'Request Invitation'
end
We run one more time and get a failure we expect. Its telling us we haven’t written any code to implement the scenario yet!
Then "gullible@lemmings.com" should receive 1 email
expected: 1
got: 0 (using ==) (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/email_steps.rb:52:in `/^(?:I|they|"([^"]*?)") should receive (an|no|\d+) emails?$/'
features/request_an_invitation.feature:9:in `Then "gullible@lemmings.com" should receive 1 email'
Dropping into RSpec unit tests
Our failing feature tells us what we need to implement so we drop down to the unit test level and start implementing it with TDD.
The feature tells us an email should be be generated so let’s go.
We’ll add to our invites_controller_spec
specifying that it should create and deliver an InviteMailer
.
spec/controllers/invites_controller_spec.rb
require 'spec_helper'
describe InvitesController do
describe 'PUT #update' do
let(:invite) { mock(:email => 'someone@someco.com', :save => true) }
let(:invite_mailer) { mock }
before do
InviteMailer.should_receive(:invite_requested).with(invite).and_return(invite_mailer)
invite_mailer.should_receive(:deliver)
Invite.should_receive(:new).with('email' => 'someone@someco.com').and_return(invite)
post :create, :invite => { :email => 'someone@someco.com' }
end
it { should redirect_to root_url }
it { should set_the_flash.to("Thanks for your interest someone@someco.com. You will hear from us soon.") }
end
end
Of course we get an error uninitialized constant InviteMailer
. We fix that by creating the mailer (it doesn’t do anything yet)
app/mailers/invite_mailer.rb
class InviteMailer < ActionMailer::Base
end
The error changes
Failure/Error: InviteMailer.should_receive(:invite_requested).with(invite).and_return(invite_mailer)
(<InviteMailer (class)>).invite_requested(#<RSpec::Mocks::Mock:0x82c96210 @name=nil>)
expected: 1 time
received: 0 times
We add the code to our controller to create and deliver the email in app/controllers/invites_controller.rb
class InvitesController < ApplicationController
def create
@invite = Invite.new(params[:invite])
if @invite.save
InviteMailer.invite_requested(@invite).deliver
redirect_to root_path, :notice => "Thanks for your interest #{@invite.email}. You will hear from us soon."
else
render :action => "new"
end
end
end
Are we done?
Checking the Cucumber Scenario…
The RSpec unit tests pass now but the Cucumber features are still failing.
When I request an invitation for "gullible@lemmings.com" # features/step_definitions/invite_steps.rb:5
undefined method `invite_requested' for InviteMailer:Class (NoMethodError)
./app/controllers/invites_controller.rb:10:in `create'
(eval):2:in `send'
(eval):2:in `click_button'
./features/step_definitions/invite_steps.rb:8:in `/^I request an invitation for "([^"]*)"$/'
features/request_an_invitation.feature:8:in `When I request an invitation for "gullible@lemmings.com"'
Duh our InviteMailer
doesn’t do anything.
Back down to the unit tests
We write our spec/mailers/invite_mailer_spec.rb
. We need to include some EmailSpec modules so we have access to its matchers.
require 'spec_helper'
describe InviteMailer do
include EmailSpec::Helpers
include EmailSpec::Matchers
describe '.invite_requested' do
let(:invite) { Factory.build :invite, :email => 'someone@someco.com' }
describe 'one email to one user' do
subject { InviteMailer.invite_requested(invite) }
it { should deliver_to invite.email }
it { should deliver_from 'alex@awesome-startup.com' }
it { should have_subject "Invitation request for Awesome New Startup received" }
it { should have_body_text "Dear someone@someco.com," }
it { should have_body_text "We have received your request" }
it { should have_body_text "Please check back at http://awesome-site-staging.heroku.com" }
end
end
end
Of course it fails because we still haven’t implemented anything. Let’s add some code to app/mailers/invite_mailer.rb
class InviteMailer < ActionMailer::Base
def invite_requested(invite)
@invite = invite
mail :to => invite.email,
:from => 'alex@awesome-startup.com',
:subject => 'Invitation request for Awesome New Startup received'
end
end
and we can use haml to format the body in app/views/invite_mailer/invite_requested.text.haml
== Dear #{@invite.email},
We have received your request to be invited into our awesome site. We'll let you know as soon as its available.
Please check back at http://awesome-site-staging.heroku.com
You must be very excited!
Thanks
An Awesome New Startup
We run the rake
one more time and …
We’re Done
Everything passes - the specs and the features!
$ rake
ruby -S rspec <a long list of _spec.rb files>
............
Finished in 2.64 seconds
12 examples, 0 failures
ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Build excitement for this vaporware
In order to drum up interest
As a user
I will receive an exciting email
Scenario: Someone requests an invitation and receives an email
Given I am on the home page
When I request an invitation for "gullible@lemmings.com"
Then "gullible@lemmings.com" should receive 1 email
And they open the email
And they should see the email delivered from "alex@awesome-startup.com"
And they should see "Invitation request for Awesome New Startup received" in the email subject
And they should see "Dear gullible@lemmings.com," in the email body
And they should see "We have received your request" in the email body
And they should see "Please check back at http://awesome-site-staging.heroku.com" in the email body
1 scenario (1 passed)
9 steps (9 passed)
0m0.293s
I hope you’ll consider using the email_spec gem
and BDD
the next time you have to add email to your app.