Skip to content

uglyog/example_pact

Repository files navigation

Example use of the Pact gem

When writing a lot of small services, testing the interactions between these becomes a major headache. That's the problem Pact is trying to solve.

Integration tests tyically are slow and brittle, requiring each component to have it's own environment to run the tests in. With a micro-service architecture, this becomes even more of a problem. They also have to be 'all-knowing' and this makes them difficult to keep from being fragile.

After J. B. Rainsberger's talk "Integrated Tests Are A Scam" people have been thinking how to get the confidence we need to deploy our software to production without having a tiresome integration test suite that does not give us all the coverage we think it does.

Pact is a ruby gem that allows you to define a pact between service consumers and providers. It provides a DSL for service consumers to define the request they will make to a service producer and the response they expect back. This expectation is used in the consumers specs to provide a mock producer, and is also played back in the producer specs to ensure the producer actually does provide the response the consumer expects.

This allows you to test both sides of an integration point using fast unit tests.

#Example Pact use#

Given we have a client that needs to make a HTTP GET request to a sinatra webapp, and requires a response in JSON format. The client would look something like:

client.rb:

require 'httparty'
require 'uri'
require 'json'

class Client


  def load_producer_json
    response = HTTParty.get(URI::encode('http://localhost:8081/producer.json?valid_date=' + Time.now.httpdate))
    if response.success?
      JSON.parse(response.body)
    end
  end


end

and the producer: producer.rb

require 'sinatra/base'
require 'json'


class Producer < Sinatra::Base


  get '/producer.json', :provides => 'json' do
    valid_time = Time.parse(params[:valid_date])
    JSON.pretty_generate({
      :test => 'NO',
      :valid_date => DateTime.now,
      :count => 1000
    })
  end

end

This producer expects a valid_date parameter in HTTP date format, and then returns some simple json back.

Running the client with the following rake task against the producer works nicely:

desc 'Run the client'
task :run_client => :init do
  require 'client'
  require 'ap'
  ap Client.new.load_producer_json
end


$ rake run_client
http://localhost:8081/producer.json?valid_date=Thu,%2015%20Aug%202013%2003:15:15%20GMT
{
          "test" => "NO",
    "valid_date" => "2013-08-15T13:31:39+10:00",
         "count" => 1000
}

Now lets get the client to use the data it gets back from the producer. Here is the updated client method that uses the returned data:

client.rb

  def process_data
    data = load_producer_json
    ap data
    value = data['count'] / 100
    date = Time.parse(data['date'])
    puts value
    puts date
    [value, date]
  end

Add a spec to test this client:

client_spec.rb:

require 'spec_helper'
require 'client'


describe Client do


  let(:json_data) do
    {
      "test" => "NO",
      "date" => "2013-08-16T15:31:20+10:00",
      "count" => 100
    }
  end
  let(:response) { double('Response', :success? => true, :body => json_data.to_json) }


  it 'can process the json payload from the producer' do
    HTTParty.stub(:get).and_return(response)
    expect(subject.process_data).to eql([1, Time.parse(json_data['date'])])
  end


end

Let's run this spec and see it all pass:

$ rake spec
/Users/ronald/.rvm/rubies/ruby-1.9.3-p448/bin/ruby -S rspec ./spec/client_spec.rb


Client
http://localhost:8081/producer.json?valid_date=Fri,%2016%20Aug%202013%2005:44:41%20GMT
{
     "test" => "NO",
     "date" => "2013-08-16T15:31:20+10:00",
    "count" => 100
}
1
2013-08-16 15:31:20 +1000
  can process the json payload from the producer


Finished in 0.00409 seconds
1 example, 0 failures

However, there is a problem with this integration point. The producer returns a 'valid_date' while the consumer is trying to use 'date', which will blow up when run for real even with the tests all passing. Here is where Pact comes in.

#Pact to the rescue#

Lets setup Pact in the consumer. Pact lets the consumers define the expectations for the integration point.

spec_helper.rb:

require 'ap'

require 'pact'
require 'pact/consumer/rspec'


$:.unshift 'lib'


Pact.configure do | config |
  config.consumer do
    name 'My Consumer'
  end
end


Pact.with_producer "My Producer" do
  mock_service :my_producer do
    port 8081
  end
end

This defines a consumer and a producer that runs on port 8081.

The spec for the client now has a pact section.

client_spec.rb:

describe 'pact with producer', :pact => true do

  let(:date) { Time.now.httpdate }

  before do
    my_producer.
      given("producer is in a sane state").
        upon_receiving("a request for producer json").
          with({
              method: :get,
              path: '/producer.json',
              query: URI::encode('valid_date=' + date)
          }).
          will_respond_with({
            status: 200,
            headers: { 'Content-Type' => 'application/json' },
            body: json_data
          })
  end

  it 'can process the json payload from the producer' do
    expect(subject.process_data).to eql([1, Time.parse(json_data['date'])])
  end

end

Running this spec still passes, but it creates a pact file which we can use to validate our assumptions on the producer side.

$ rake spec
/Users/ronald/.rvm/rubies/ruby-1.9.3-p448/bin/ruby -S rspec ./spec/client_spec.rb


Client
http://localhost:8081/producer.json?valid_date=Fri,%2016%20Aug%202013%2006:09:44%20GMT
{
  "test"  => "NO",
  "date"  => "2013-08-16T15:31:20+10:00",
  "count" => 100
}
1
2013-08-16 15:31:20 +1000
  can process the json payload from the producer
  pact with producer
http://localhost:8081/producer.json?valid_date=Fri,%2016%20Aug%202013%2006:09:44%20GMT
{
  "test"  => "NO",
  "date"  => "2013-08-16T15:31:20+10:00",
  "count" => 100
}
1
2013-08-16 15:31:20 +1000
    can process the json payload from the producer

Generated pact file (spec/pacts/my_consumer-my_producer.json):

{
  "producer": {
    "name": "My Producer"
  },
  "consumer": {
    "name": "My Consumer"
  },
  "interactions": [
    {
      "description": "a request for producer json",
      "request": {
        "method": "get",
        "path": "/producer.json",
        "query": "valid_date=Fri,%2016%20Aug%202013%2006:09:44%20GMT"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": {
          "test": "NO",
          "date": "2013-08-16T15:31:20+10:00",
          "count": 100
        }
      },
      "producer_state": "producer is in a sane state"
    }
  ],
  "metadata": {
    "date": "2013-08-16T16:09:44+10:00",
    "pact_gem": {
      "version": "0.1.23"
    }
  }
}

#Producer Setup#

Pact has a rake task to verify the producer against the generated pact file. It can get the pact file from any URL (like the last sucessful CI build), but we just going to use the local one. Here is the addition to the Rakefile.

Rakefile:

require 'pact'
require 'pact/verification_task'


Pact::VerificationTask.new(:local) do | pact |
  pact.uri 'spec/pacts/my_consumer-my_producer.json', support_file: './spec/pacts/pact_helper'
end

The pact_helper needs to tell Pact which Rack app it needs to test against.

spec/pacts/pact_helper.rb

Pact.with_consumer 'My Consumer' do
  producer_state "producer is in a sane state" do
    set_up do
      # Create a thing here using your factory of choice
    end
  end
end


require 'producer'
Pact.configure do | config |
  config.producer do
    name "My Producer"
    app { Producer.new }
  end
end

Now if we run our pact verification task, it should fail.

$ rake pact:verify:local


Pact in spec/pacts/my_consumer-my_producer.json
  Given producer is in a sane state
    a request for producer json to /producer.json
      returns a response which
        has status code 200
        has a matching body (FAILED - 1)
        includes headers
          "Content-Type" with value "application/json" (FAILED - 2)


Failures:


  1) Pact in spec/pacts/my_consumer-my_producer.json Given producer is in a sane state a request for producer json to /producer.json returns a response which has a matching body
     Failure/Error: expect(parse_entity_from_response(last_response)).to match_term response['body']
       {
         "date"  => {
           :expected => "2013-08-16T15:31:20+10:00",
           :actual   => nil
         },
         "count" => {
           :expected => 100,
           :actual   => 1000
         }
       }

Looks like we need to update the producer to return 'date' instead of 'valid_date', we also need to update the client expectation to return 1000 for the count and the correct content type (we expected application/json but got application/json;charset=utf-8). Doing this, and we now have fast unit tests on each side of the integration point instead of tedious integration tests.

About

Example use of the Pact gem

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages