I was recently tasked with this story:
Background:
Given I go to the sign up page
And I fill in "Email" with "new@example.com"
And I fill in "Password" with "password"
Scenario: Visitor signs up and does not want email
When I press "Sign up"
Then email service is not notified "new@example.com" signed up
Scenario: Visitor signs up and wants email
When I check "Please email me"
And I press "Sign up"
Then email service is notified "new@example.com" signed up
The email service in question was internal, accessible via an HTTP API, and did not have a Ruby client library.
So, I had to write and test my own. The usual approach would involve using a HTTP stubbing library like Sham Rack or Artifice. I decided to try something different and see how it felt.
First, I needed an interface that I could test:
Then /^email service is notified "([^"]*)" signed up$/ do |email|
EmailService.notifications.should include(email)
end
Then /^email service is not notified "([^"]*)" signed up$/ do |email|
EmailService.notifications.should_not include(email)
end
I’m re-using a pattern (“store data in a simple array for easy state-based testing”) we’ve used before for Javascript integration testing Mixpanel.
So I have the start to an EmailService
interface. I thought I wanted it
invoked as part of an after_save
callback on the User
model, so here’s that
spec:
describe User, 'who opts into email' do
subject { build(:user, email_opt_in: true) }
before do
EmailService.stubs(:notify)
subject.save
end
it 'notifies EmailService' do
EmailService.should have_received(:notify).with(subject.email)
end
end
describe User, 'who does not opt into email' do
subject { build(:user, email_opt_in: false) }
before do
EmailService.stubs(:notify)
subject.save
end
it 'does not notify EmailService' do
EmailService.should have_received(:notify).never
end
end
This is the stubbing and spying technique and uses RSpec, mocha, and bourne.
My thought process was that I needed an active verb, notify
to invoke when the
user is created, but that will store the invocation in a notifications
array
that the Cucumber step definition needs to check state (I don’t want to stub,
spy, or mock in an integration test).
So, making the user spec pass isn’t bad:
require 'email_service/notifier'
class User < ActiveRecord::Base
after_create do
if email_opt_in?
EmailService.notify(email)
end
end
end
Now, the EmailService
can be spec'ed. We already had John
Nunemaker’s
HTTParty as a dependency in the app, and I
only had to make one HTTP POST, so I knew I would be re-using HTTParty’s
interface.
describe EmailService::Notifier, '#post' do
subject { EmailService::Notifier }
let(:email) { 'new-signup@example.com' }
before do
subject.stubs(:post)
subject.new(email).post
end
it 'POSTs to email service with API_KEY and given email' do
subject.should have_received(:post).with(
subject::URL, query: { api_key: subject::PARAMS, email: email }
)
end
end
What I care about here is that during the one POST the app has to make, that the parameters are correct. This is close to hitting the live service as I’m willing to get without making an HTTP request.
I had an internal debate with myself while writing it over whether this is
“stubbing the system under test”
(considered bad practice). I decided “no” because despite the subject
being
stubbed, the system under test is actually the EmailService::Notifier#post
method.
The stubbed and spied method is also mixed in from HTTParty, so I feel clean with this approach.
Making it pass:
require 'httparty'
module EmailService
class << self
attr_accessor :notifications
end
def self.notify(email, live = false)
if Rails.env.production? || live
Notifier.new(email).post
else
self.notifications << email
end
end
class Notifier
include HTTParty
API_KEY = 12345
URL = 'http://emailservice.example.com'
def initialize(email)
@url = URL
@email = email
end
def post
self.class.post(@url, query: { api_key: API_KEY, email: @email })
end
end
end
Summary
- I’ve got an environment-specific conditional inside the true public interface
for the service,
EmailService.notify
, which is used by theUser
model. - In my integration test and in the development environment, all that happens is an array is populated, which makes it easy to confirm that it was notified correctly.
- I’ve got a unit test that makes sure the correct URL and API key are used.
- I’ve got a way to override
EmailService.notify
with thelive
flag so I can invoke it from the Rails console on production or staging when testing or debugging.
This almost takes longer to describe than to code but I’m curious what people think about this style of writing an API client with test, staging, and production environments in mind. How do you do things differently?