Skip to content

Instantly share code, notes, and snippets.

@bethesque
Last active October 21, 2023 16:03
Show Gist options
  • Save bethesque/0ee446a9f93db4dd0697 to your computer and use it in GitHub Desktop.
Save bethesque/0ee446a9f93db4dd0697 to your computer and use it in GitHub Desktop.
Using Pact with non-HTTP services

When you declare a request and response using the traditional Pact DSL, ("uponReceiving" and "willRespondWith") you're building a structure that has three purposes -

  1. it provides the concrete example request and response used in the tests
  2. it specifies the contents of the contract which...
  3. defines how to validate the the actual request/response against the expected request/response

The three different uses of this structure are hidden from you when using HTTP Pact because the mock service handles numbers 1 & 2 in the consumer tests, and the verification task handles number 3 for you in the provider tests. When using Pact in a non-HTTP scenario, there is no nice neat protocol layer to inject the code to do this for you, so you have to explicitly do each step.

The file expected_data_from_collector.rb declares an object graph using the Pact DSL. This is going to be used to create the concrete example and the contract. This could be declared inline, but for easier maintenance, and to allow the contract publishing code to easily access it, it's in a separate file.

The file consumer.rb takes that object graph declared in expected_data_from_collector.rb, and generates an example JSON document from it (that's what FixtureHelpers.load_generated_pact_fixture does). It then uses that JSON document in a test to show that it can handle the document we expect.

The file consumer_publish_contract.rb shows how we take that same object graph and turn it into the JSON Pact-like contract, and publish it to the pact broker.

The file fixture_helpers.rb shows the code that is used to turn the object graph declared using the Pact DSL into the example structure.

The file z_provider.rb contains a test that uses the contract that we retrieve from the pact broker. It shows that the output that we produce contains all the data that the consumer expects (of course, it may contain extra data without it being a problem).

# Consumer - a test to show that the consumer can correctly handle the data we expect
require 'load'
require 'json'
describe "using the data we expect from the Credit Collector" do
let(:record) { FixtureHelpers.load_generated_pact_fixture('expected_data_from_collector.rb').first }
let(:inserted_record) { connection[table_definition.table_name].first }
subject { Load.call([record]) }
it "inserts the given data into the database" do
expect(inserted_record[:approval_date]).to eq Date.new(2015, 1, 2)
expect(inserted_record[:cid]).to eq record.fetch('cid')
expect(inserted_record[:account_manager_name]).to eq record.fetch('account_manager_name')
expect(inserted_record[:product]).to eq record.fetch('product')
expect(inserted_record[:description]).to eq record.fetch('description')
end
end
# Consumer - publish the contract. This would be called from a Rake task eg. rake consumer_contract:publish
require 'uri'
require 'net/http'
require 'spec/support/fixture_helpers.rb'
class PublishCollectorContract
PACT_BROKER_BASE_URL = 'http://pact-broker'
def self.call
data = FixtureHelpers.load_ruby_fixture('expected_data_from_collector.rb')
contract_json = { data: data }.to_json
path = URI.encode("/pacts/provider/Credit%20Collector/consumer/Credit%20Loader/version/#{ENV.fetch('BUILD_NUMBER')}")
uri = URI(PACT_BROKER_BASE_URL + path)
publish uri, contract
end
def self.publish uri, contract
response = http_put uri, contract
if response.is_a?(Net::HTTPSuccess)
puts "Published contract to #{uri}"
else
puts response.body
raise "Error publishing contract"
end
end
def self.http_put uri, body
post_req = Net::HTTP::Put.new(uri.path)
post_req['Content-Type'] = "application/json"
post_req.body = body
response = Net::HTTP.start(uri.hostname, uri.port) do |http|
http.request post_req
end
end
end
{
"data": [
{
"approval_date": {
"json_class": "Pact::Term",
"data": {
"generate": "2015-01-02",
"matcher": {
"json_class": "Regexp",
"o": 0,
"s": "\\d{4}\\-\\d{2}\\-\\d{2}"
}
}
},
"cid": {
"json_class": "Pact::Term",
"data": {
"generate": "123456",
"matcher": {
"json_class": "Regexp",
"o": 0,
"s": "^\\d+$"
}
}
},
"amount": {
"json_class": "Pact::Term",
"data": {
"generate": "123.45",
"matcher": {
"json_class": "Regexp",
"o": 0,
"s": "\\d+(\\.\\d{1,4})?"
}
}
},
"product": {
"json_class": "Pact::SomethingLike",
"contents": "A product"
},
"account_manager_name": {
"json_class": "Pact::SomethingLike",
"contents": "Mary Jones"
},
"description": {
"json_class": "Pact::SomethingLike",
"contents": "Mary Jones"
}
}
]
}
# Consumer - the Pact fixture from which we generate
# 1. an example to use in our consumer spec
# 2. the contract with the provider
require 'pact'
[{
"approval_date" => Pact::Term.new(generate: "2015-01-02", matcher: /\d{4}\-\d{2}\-\d{2}/),
"cid" => Pact::Term.new(generate: "123456", matcher: /^\d+$/),
"amount" => Pact::Term.new(generate: "123.45", matcher: /\d+(\.\d{1,4})?/),
"product" => Pact::SomethingLike.new("A product"),
"account_manager_name" => Pact::SomethingLike.new("Mary Jones"),
"description" => Pact::SomethingLike.new("Mary Jones"),
}]
# Consumer - fixture helpers
module FixtureHelpers
extend self
def load_fixture(file_name)
File.read("./spec/fixtures/#{file_name}")
end
def load_ruby_fixture(file_name, options = {})
proc = Proc.new {}
eval(load_fixture(file_name), proc.binding, file_name)
end
# Turns the Pact::Terms and Pact::SomethingLikes into actual values.
def load_generated_pact_fixture(file_name, options = {})
require 'pact'
::Pact::Reification.from_term(load_ruby_fixture(file_name, options))
end
end
# Provider - verify that the provider can actually create the document the consumer expects
require 'run'
require 'pact'
require 'open-uri'
describe "Collecting credit data" do
let(:publisher_contract_url) { 'http://pact-broker/pacts/provider/Credit%20Collector/consumer/Credit%20Loader/latest' }
let(:publisher_contract) { open(publisher_contract_url) { | file | JSON.load(file.read) } }
let(:data_contract) { publisher_contract['data'] }
let(:output_records) { JSON.parse(File.read("output.json")) } # Assume this is where the output was written by Run.call
before do
# all the data set up and stubbing here
end
subject { Run.call }
it "creates a file in the format that the Credit Loader expects" do
subject
diff = Pact::JsonDiffer.call(data_contract, output_records)
puts Pact::Matchers::UnixDiffFormatter.call(diff) if diff.any? # Print a pretty diff if we fail
expect(diff).to be_empty
end
end
@bethesque
Copy link
Author

Please remember that gist comments and mentions don't trigger notifications. You can use the Google Pact group or Gitter to ask questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment