“Captain! All the integration tests have failed again! Could it be just another false negative?”
“Well, you just never know, Son. Fire all the torpedoes anyway”
I feel the way we do integration testing is sort of like driving your car from Melbourne to Sydney each time your mechanic makes a change to it. Or setting your house on fire to test your smoke alarm. It is excessive, tiresome and way too costly.
In a previous blog I talked about using the Pact gem to validate integration points in your architecture (see Testing interactions with web services without integration tests in ruby).
Since then Pact has improved a lot, and the original example was a bit contrived. Well, totally contrived. I thought it time to provide a more concrete example using the latest version of Pact.
What is Pact?
From the Pact website:
Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
Essentially, Pact provides a mechanism for creating a contract between a service consumer and a service provider, and then providing the tools to validate that the consumer and provider adhere to the contact independently of each other.
Pact, a tale of two parts – courtesy Bethany Skurrie
An example: Splitting out events to an Event Service
For one of our clients, we were required to create a RESTful Web API for customers to get a feed of events that have occurred in their web application. In this particular case, the web application was extremely large and complex, as we did not want to add more to it. We decided to create a new small service in Ruby that would house the API, and we would push events to it via HTTP as they occurred.
Event Architecture
The source system would do a POST request to an event resource URI, and this would create a new event in the event store. This new event would then be available via a feed to users of the API. The API would use a HMAC authentication scheme, and the events would be sent and stored in JSON format. For the Ruby HMAC library I used, refer to https://github.com/Asquera/warden-hmac-authentication.
Testing the interactions between the source system and the event API
The source system is a large web application that requires a lot of running components to function. And our new web API required a data store to store the events. We sure didn’t want to create a test environment with all of the services running in it just to make sure we haven’t broken the interaction between the two applications.
The Consumer
For this particular interaction, the consumer is the source system which is going to push the events to the event API. It will use a HTTP Client library (HTTParty in this case) to make the request and include all the necessary headers for HMAC authentication scheme. Here is a very simple version that posts a JSON version of an event and returns a boolean.
require 'httparty'
require 'hmac/signer'
require 'digest'
class SourceSystemClient
include HTTParty
base_uri 'http://events-service'
attr_reader :hmac_secret
def save_event(event)
json_body = event.to_json
response = self.class.post('/events', body: json_body, headers: hmac_headers(json_body))
response.code == 200
end
def hmac_headers(json_body)
hmac = HMAC.new
md5 = Digest::MD5.hexdigest json_body
now = Time.now
headers = { 'Content-Md5' => md5 }
signature => hmac.generate_signature(secret: @hmac_secret, method: 'POST', date: now, path: '/events', headers: headers)
headers.merge 'X-Hmac-Date' => now.httpdate, 'Authorization' => "HMAC #{signature}", 'Content-Type' => 'application/json'
end
end
view rawclient.rb hosted with ❤ by GitHub
The event model looks something like this:
require 'securerandom'
class Event
attr_accessor :event_id, :timestamp, :host, :remote_address, :event_type, :user
def initialize
@event_id = SecureRandom.uuid
@timestamp = Time.now
end
def to_hash
{
eventId: @event_id,
timestamp: @timestamp.gmtime,
host: @host,
remoteAddress: @remote_address,
eventType: @event_type,
user: @user
}
end
end
view rawevent.rb hosted with ❤ by GitHub
This event generates the following JSON (running on my laptop):
{
"eventId": "13ec9646-a33b-4319-a941-dbb924a60130",
"timestamp": "2014-05-13 01:06:38 UTC",
"host": "rholshausen.local",
"remoteAddress": "0:0:0:0:0:0:0:1%0",
"eventType": "LOGIN",
"user": "rholshausen"
}
view rawevent.json hosted with ❤ by GitHub
The actual request that gets sent is:
POST /events HTTP/1.1
Content-Md5: CHEFLZW9taJ4sH6fxPN82Q==
X-Hmac-Date: Tue, 13 May 2014 01:21:17 GMT
Authorization: HMAC 1f5ca8529707d870bb993b10fd17ddf53971e4b4
Content-Type: application/json
Connection: close
Host: localhost:9292
Content-Length: 192
{"eventId":"13ec9646-a33b-4319-a941-dbb924a60130","timestamp":"2014-05-13 01:06:38 UTC","host":"rholshausen.local","remoteAddress":"0:0:0:0:0:0:0:1%0","eventType":"LOGIN","user":"rholshausen"}
view rawexample post hosted with ❤ by GitHub
The Pact
We will start with the Consumer and create a Pact test which will define the contract between the source system and the RESTful web API. We’ll use the Pact DSL which integrates nicely into rspec. First we create a pact helper:
require 'pact/consumer/rspec' # Require the pact rspec helper
Pact.service_consumer "Source System" do # register a consumer with pact
has_pact_with "Event API" do # register the provider that has the pact
mock_service :event_api do # register the mock service that will run and pretend to be the provider
port 1234
end
end
end
view rawpact_helper.rb hosted with ❤ by GitHub
When the spec runs, this will start a mock HTTP server listening on port 1234. The spec file follows:
require 'pact_helper'
require_relative '../lib/client'
require_relative '../lib/event'
describe SourceSystemClient, :pact => true do
before do
subject.class.base_uri 'http://localhost:1234'
end
describe 'a request to store an event' do
let(:event) { Event.new }
let(:event_json) { event.to_hash }
before do
event_api.upon_receiving('A POST request with an event').
with(method: :post, path: '/events', headers: {'Content-Type' => 'application/json'}, body: event_json).
will_respond_with(status: 200, headers: {'Content-Type' => 'application/json'})
end
it 'is successful' do
expect(subject.save_event(event)).to be_true
end
end
end
view rawconsumer_spec.rb hosted with ❤ by GitHub
Running this test passes, and it generates a pact file of the interaction.
{
"provider": {
"name": "Event API"
},
"consumer": {
"name": "Source System"
},
"interactions": [
{
"description": "A POST request with an event",
"request": {
"method": "post",
"path": "/events",
"body": {
"eventId": "7622cb68-3ee5-4f93-a32b-da82f3ff95b5",
"timestamp": "2014-05-13 02:03:33 UTC",
"host": null,
"remoteAddress": null,
"eventType": null,
"user": null
},
"headers": {
"Content-Type": "application/json"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
}
}
}
],
"metadata": {
"pactSpecificationVersion": "1.0.0"
}
}
view rawpact.json hosted with ❤ by GitHub
The Provider
Our little Ruby web service is going to provide an event resource that can accept POST requests containing the events. It will validate the request and authenticate it using a HMAC scheme. We’re using Grape for the API and Warden HMAC gem for the HMAC verification . We will also use the ROAR gem for converting the JSON into an Event model.
require 'grape'
require 'models/event_repository'
require 'models/events_decorator'
require 'digest/md5'
class EventsApi < Grape::API
version 'v1', using: :accept_version_header
helpers do
def body_matches_content_md5(body)
content_md5 = request.env['HTTP_CONTENT_MD5']
body_md5 = Base64.encode64(Digest::MD5.digest(body)).chomp
body_md5 == content_md5
end
end
content_type :json, 'application/json'
default_format :json
parser :json, nil
# enable warden HMAC authentication
before do
env['warden'].authenticate! :scope => :hmac
end
desc 'singleEvent: Store a single Event'
post do
# Check that the MD5 for the body actually matches the header. This is important as the HMAC signature includes the MD5 checksum header, but the
# warden HMAC does not verify it matches the body
error!('Unauthorized - Content-MD5 header is required for POST', 401) unless body_matches_content_md5(env['api.request.body'])
event = Models::Event.new.extend(Models::EventDecorator).from_json(env['api.request.body'])
Models::EventRepository.instance.add_event(event)
end
end
view rawprovider.rb hosted with ❤ by GitHub
We can now verify the pact file generated in our consumer test against our provider. Pact has a very convenient rake task to do this (see the section in Pact service provider project in the pact documentation). We just need to setup a helper in spec/service_consumers/pact_helper.rb to configure the verification task with the path to the pact file.
Running the pact verify task gives us two errors:
The second error is easy to fix. In our client we don’t actual expect a response body, only the success (200) HTTP code. So expecting a content type header is incorrect. We can fix that in our consumer pact test by removing the header. Also, a POST to a Grape API returns a 201, not a 200. So we should change that too.
The first error is due to the missing authentication headers, so we got an unauthenticated (401) response. We’ll need to add those to our consumer spec.
Getting the Pact tests to pass
I’ll update the client to expect a 201 instead of a 200.
require 'httparty'
require 'hmac/signer'
require 'digest'
require 'json'
class SourceSystemClient
include HTTParty
debug_output $stderr
base_uri 'http://localhost:9292'
attr_accessor :hmac_secret
def save_event(event)
json_body = JSON.generate(event.to_hash)
response = self.class.post('/events', body: json_body, headers: hmac_headers(json_body))
response.code == 201
end
def hmac_headers(json_body)
hmac = HMAC::Signer.new
md5 = Digest::MD5.base64digest json_body
now = Time.now
headers = { 'Content-Md5' => md5 }
signature = hmac.generate_signature(secret: @hmac_secret, method: 'POST', date: now.httpdate, path: '/events', headers: headers)
headers.merge 'X-Hmac-Date' => now.httpdate, 'Authorization' => "HMAC #{signature}", 'Content-Type' => 'application/json'
end
end
view rawclient_updated.rb hosted with ❤ by GitHub
We just need to get the HMAC Authentication headers added to our consumer pact test. Our client class already calculates these headers, so they will be easy to add. Here is the updated spec:
require 'pact_helper'
require_relative '../lib/client'
require_relative '../lib/event'
describe SourceSystemClient, :pact => true do
let!(:now) { Time.now }
before do
subject.class.base_uri 'http://localhost:1234'
subject.hmac_secret = '01234567890'
Time.stub(:now).and_return(now)
end
describe 'posting an event to the event service' do
let(:event) { Event.new }
let(:event_json) { event.to_hash }
before do
event_api.upon_receiving('A POST request with an event').
with(method: :post, path: '/events', headers: subject.hmac_headers(JSON.generate(event_json)), body: event_json).
will_respond_with(status: 201)
end
it 'is successful' do
expect(subject.save_event(event)).to be_true
end
end
end
view rawconsumer_spec_updated.rb hosted with ❤ by GitHub
Running this generates the following pact file:
{
"provider": {
"name": "Event API"
},
"consumer": {
"name": "Source System"
},
"interactions": [
{
"description": "A POST request with an event",
"request": {
"method": "post",
"path": "/events",
"headers": {
"Content-Md5": "QRYlWyL4AwKkqs2sfV6WBA==",
"X-Hmac-Date": "Tue, 13 May 2014 05:57:15 GMT",
"Authorization": "HMAC a59a8cb934fa910d62a61fd19bbfbb23ba1c4caa",
"Content-Type": "application/json"
},
"body": {
"eventId": "80573745-eaee-4e5e-8000-d0eb938c826b",
"timestamp": "2014-05-13 05:57:15 UTC",
"host": null,
"remoteAddress": null,
"eventType": null,
"user": null
}
},
"response": {
"status": 201
}
}
],
"metadata": {
"pactSpecificationVersion": "1.0.0"
}
}
view rawupdated_pact.json hosted with ❤ by GitHub
Now, coping this latest pact over to the provider project and re-running the verification task, we get a success, Yay!
$ rake pact:verify
Using spec/service_consumers/pact_helper.rb
Verifying a pact between Source System and Event API
A post request with an event
returns a response which
has status code 201
Finished in 0.10356 seconds
1 example, 0 failures
view rawspec run pass hosted with ❤ by GitHub
In Conclusion
Instead of having to build a test environment running with two servers and databases to test that our interaction between our source system and our event API works, we now have two fast tests that achieve the same result. As long as we maintain the pact file between the two systems, we can be reasonably sure that our integration is working correctly.
There is a number of ways of maintaining the pact file. We can get our consumer build to publish the pact file to the CI server as a build artefact. The pact verification task supports loading the pact file from a URL. That way we can get it to always use the pact file from the latest successful build.
There is also a Pact Broker project that provides a repository of pact files. It allows you to be able to verify your providers against pact files from different versions of your consumer, statically check the compatibility of consumers and providers, and automate your CI and CD workflows.
NOTE: The Pact Broker is an open source tool that requires you to deploy, administer and host it yourself. If you would prefer a plug-and-play option, we’ve created Pactflow, a fully managed Pact Broker with additional features to simplify teams getting started and scaling with Pact and contract testing. You can get started quickly and for free on our Developer Plan