0.02
No commit activity in last 3 years
No release in over 3 years
Enables consumer driven contract testing for asynchronous message driven systems.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.2
~> 0.10
~> 1.6
~> 10.0
~> 0.0.12
~> 3.3

Runtime

~> 1.9
 Project Readme

Pact::Messages

Build Status

Define a pact contract between non-HTTP asynchronous service consumers and providers, enabling "consumer driven contract" testing.

This is an extension for Pact gem which covers HTTP scenario.

This allows testing shape of your JSON messages on both sides of an integration point using fast unit tests.

This gem is inspired by the concept of "Consumer driven contracts". See this article by Ian Robinson for more information.

What is it good for?

Pact is most valuable for designing and testing integrations where you (or your team/organisation/partner organisation) control the development of both the consumer and the provider, and the requirements of the consumer are going to be used to drive the features of the provider.

You can find this solution very useful for systems based on messaging platforms aimed at providing an integration and communication between various business components, e.g. RabbitMQ messaging system.

How does it work?

  1. In the specs for the provider facing code in the consumer project, expectations are set up on a mock service provider.

  2. When the specs are run, the mock service stores pact contracts in the Contract Repository and writes contract in to a "pact" file.

  3. In specs you are able to get pact contract from Contract Repository and verify shape of you message against this contract.

  4. You are also able to build a Sample Message from the contract, e.g. build JSON message like {foo: 'bar'} from the contract: {foo: like('bar')}.

Installation

Add this line to your application's Gemfile:

gem 'pact-messages'

And then execute:

$ bundle

Or install it yourself as:

$ gem install pact-messages

Usage

Define Pact contract

# in /spec/service_providers/pact_helper.rb
# or
# in /spec/support/pact_helper.rb

require 'pact/messages'

Pact::Messages.service_consumer 'Message Consumer' do
  has_pact_with 'Message Provider' do
    mock_service 'message_provider_service'
  end
end

To modify the default pact broker url (http://pact-broker) please set Pact::Messages.pact_broker_url:

Pact::Messages.pact_broker_url = 'http://my-pact-broker'

Define Mock Service

Pact::Messages.build_mock_service(:message_provider_service) do |service|
  service.given('User subscribed')
    .description('a request for subscribed user')
    .provide(
      {
        'first_name' => like('John'),
        'last_name'  => like('Smith'),
        'subscribed' => like(true),
      },
    )

  service.given('User unsubscribed')
    .provide(
      {
        'first_name' => like('John'),
        'last_name'  => like('Smith'),
        'subscribed' => like(false),
      },
    )
end

PS: '.given' is optional, if you have only one state, you don't need to specify '.given' '.description' is optional

Pact::Messages.build_mock_service(:message_provider_service) do |service|
  service.provide(
    {
      'first_name' => like('John'),
      'last_name'  => like('Smith'),
      'subscribed' => like(true),
    }
  )
end

Verify contract on the Provider side

  module MessageBuilder
    def self.build(subscribed)
      {
        'first_name' => 'William',
        'last_name'  => 'Taylor',
        'subscribed' => subscribed,
      }
    end
  end
end

Rspec

describe MessageBuilder, pact: true do
  subject { described_class.build(subscribed_status) }

  describe ".build" do
    context "subscribed" do
      let(:subscribed_status) { true }
      let(:user_contract) do
        Pact::Messages.get_message_contract('Message Provider', 'Message Consumer', 'User subscribed')
      end

      it 'matches the contract' do
        diff = Pact::JsonDiffer.call(user_contract, subject)
        puts Pact::Matchers::UnixDiffFormatter.call(diff) if diff.any? # Print a pretty diff if we fail
        expect(diff).to be_empty
      end
    end

    context "unsubscribed" do
      let(:subscribed_status) { false }
      let(:user_contract) do
        Pact::Messages.get_message_contract('Message Provider', 'Message Consumer', 'User unsubscribed')
      end

      it 'matches the contract' do
        diff = Pact::JsonDiffer.call(user_contract, subject)
        puts Pact::Matchers::UnixDiffFormatter.call(diff) if diff.any? # Print a pretty diff if we fail
        expect(diff).to be_empty
      end
    end
  end
end

Using Sample from the Contract on Consumer side

module MessageProcessor
  def self.full_name(message)
    [message.fetch('first_name'), message.fetch('last_name')].join(' ')
  end
end

Rspec

describe MessageProcessor, pact: true do
  let(:message) do
    Pact::Messages.get_message_sample('Message Provider', 'Message Consumer', 'User subscribed')
  end

  describe '.full_name' do
    it 'joins first name and last name' do
      expect(described_class.full_name(message)).to eq('John Smith')
    end
  end
end

License

The gem is available as open source under the terms of the MIT License.