Testing Email with Rspec 3

It was a strange day for me but I wrote a post on August 2, 2011 on how to test email with RSpec.

Nearly 3 years later, I can look back and say it’s not so bad but it no longer works. There are many things deprecated and permanent removed with RSpec 3.0.

Now that I have better sense of structure, I would have never written integration tests that way. The post Testing Some Devise Features With RSpec, Steak and Email Spec is a reminder of why I write often.

On an existing Rails application, you can simply add this on your Gemfile.

group :test do
  gem 'email_spec'
  gem 'factory_girl_rails'
  gem 'rspec-rails', '~> 3.0.0'
 # other gems
end

Install the gems and read about RSpec features if you are not familiar with it. There is no need for the Steak gem. I think it has no advantages anymore and may simply exist because people want to mock or dislike Cucumber.

A typical feature for testing sign up (Devise) may look like this:

require 'rails_helper'

feature "Sign up" do
  let(:user) { build(:user) }
  scenario "User is not registered" do
    visit sign_up_path
    reset_mailer
    fill_in 'user_email', with: user.email
    fill_in 'user_password', with: user.password
    fill_in 'user_password_confirmation', with: user.password
    click_button 'Sign up'
    expect(page).to have_text("A message with a confirmation link has been sent to your email address.
                               Please open the link to activate your account")
    expect(unread_emails_for(user.email).count).to eq(1)

    open_email(user.email)
    expect(current_email).to have_body_text("You can confirm your account email through the link below")
    click_first_link_in_email

     expect(page).to have_content('Sign in')

     fill_in 'user_email', with: user.email
     fill_in 'user_password', with: user.password
     click_button 'Sign in'
     expect(page).to have_text("Sign out")
  end
end

It works. Notice that on the old post, I used the should syntax which will no longer work. The new syntax is easier to understand for beginners. This is the primary reason for the change.

The example above works but it is not DRY. You need to create a helper.

# spec/support/features/session_helpers.rb
module Features
  module SessionHelpers
    def sign_up(email, password)
      visit sign_up_path
      fill_in 'user_email', with: email
      fill_in 'user_password', with: password
      fill_in 'user_password_confirmation', with: password
      click_button 'Sign up'
    end

    def sign_in(user)
      visit sign_in_path
      fill_in 'user_email', with: user.email
      fill_in 'user_password', with: user.password
      click_button 'Sign in'
    end
  end
end

Update the RSpec config to include this helper.

RSpec.configure do |config|
 # Other config options
  config.include(EmailSpec::Helpers)
  config.include(EmailSpec::Matchers)
  config.include Features::SessionHelpers, type: :feature
end

Now it is possible to reuse this for other features.

require 'rails_helper'

feature "Sign up" do
  let(:user) { build(:user) }
  scenario 'User is not registered' do
    reset_mailer
    sign_up(user.email, user.password)
    expect(page).to have_text('A message with a confirmation link has been sent to your email address.
                               Please open the link to activate your account')
    expect(unread_emails_for(user.email).count).to eq(1)
    open_email(user.email)
    expect(current_email).to have_body_text('You can confirm your account email through the link below')
    click_email_link_matching(/#{user_confirmation_path}/)
    expect(page).to have_content('Sign in')
    sign_in(user)
    expect(page).to have_content('Sign out')
  end
end

Using the helper click_email_link_matching is much better because most of the time, we need to make sure it’s correct.