Skip to content
This repository has been archived by the owner on Dec 5, 2019. It is now read-only.

thoughtworks/pacto

Repository files navigation

Gem Version Build Status Code Climate Dependency Status Coverage Status

Pacto is currently INACTIVE. Unfortunately none of the maintainers have had enough time to maintain it. While we feel that Pacto had good ideas, we also feel that a lot has changed since Pacto was first conceived (including the OpenAPIs initiative) and that too much work would be required to maintain & update Pacto. Instead, we encourage others to use other projects that have focused on Consumer-Driven Contracts, like Pact, or to write their own Pacto-inspired project.

[INACTIVE] Pacto

Pacto is a judge that arbitrates contract disputes between a service provider and one or more consumers. In other words, it is a framework for Integration Contract Testing, and helps guide service evolution patterns like Consumer-Driven Contracts or Documentation-Driven Contracts.

Pacto considers two major terms in order decide if there has been a breach of contract: the request clause and the response clause.

The request clause defines what information must be sent by the consumer to the provider in order to compel them to render a service. The request clause often describes the required HTTP request headers like Content-Type, the required parameters, and the required request body (defined in json-schema) when applicable. Providers are not held liable for failing to deliver services for invalid requests.

The response clause defines what information must be returned by the provider to the consumer in order to successfully complete the transaction. This commonly includes HTTP response headers like Location as well as the required response body (also defined in json-schema).

Test Doubles

The consumer may also enter into an agreement with test doubles like WebMock, vcr or mountebank. The services delivered by the test doubles for the purposes of development and testing will be held to the same conditions as the service the final services rendered by the parent provider. This prevents misrepresentation of sample services as realistic, leading to damages during late integration.

Pacto can provide a test double if you cannot afford your own.

Due Diligence

Pacto usually makes it clear if the consumer or provider is at fault, but if a contract is too vague Pacto cannot assign blame, and if it is too specific the matter may become litigious.

Pacto can provide a contract writer that tries to strike a balance, but you may still need to adjust the terms.

Implied Terms

  • Pacto only arbitrates contracts for JSON services.
  • Pacto requires Ruby 1.9.3 or newer, though you can use Polyglot Testing techniques to support older Rubies and non-Ruby projects.

Roadmap

  • The test double provided by Pacto is only semi-competent. It handles simple cases, but we expect to find a more capable replacement in the near future.
  • Pacto will provide additional Contract Writers for converting from apiblueprint, WADL, or other documentation formats in the future. It's part of our goal to support Documentation-Driven Contracts
  • Pacto reserves the right to consider other clauses in the future, like security and compliance to industry specifications.

Usage

See also: http://thoughtworks.github.io/pacto/usage/

Pacto can perform three activities: generating, validating, or stubbing services. You can do each of these activities against either live or stubbed services.

You can also use Pacto Server if you are working with non-Ruby projects.

Configuration

In order to start with Pacto, you just need to require it and optionally customize the default Configuration. For example:

require 'pacto'

Pacto.configure do |config|
  config.contracts_path = 'contracts'
end

Generating

The easiest way to get started with Pacto is to run a suite of live tests and tell Pacto to generate the contracts:

Pacto.generate!
# run your tests

If you're using the same configuration as above, this will produce Contracts in the contracts/ folder.

We know we cannot generate perfect Contracts, especially if we only have one sample request. So we do our best to give you a good starting point, but you will likely want to customize the contract so the validation is more strict in some places and less strict in others.

Contract Lists

In order to stub or validate a group of contracts you need to create a ContractList. A ContractList represent a collection of endpoints attached to the same host.

require 'pacto'

default_contracts = Pacto.load_contracts('contracts/services', 'http://example.com')
authentication_contracts = Pacto.load_contracts('contracts/auth', 'http://example.com')
legacy_contracts = Pacto.load_contracts('contracts/legacy', 'http://example.com')

Validating

Once you have a ContractList, you can validate all the contracts against the live host.

contracts = Pacto.load_contracts('contracts/services', 'http://example.com')
contracts.simulate_consumers

This method will hit the real endpoint, with a request created based on the request part of the contract.
The response will be compared against the response part of the contract and any structural difference will generate a validation error.

Running this in your build pipeline will ensure that your contracts actually match the real world, and that you can run your acceptance tests against the contract stubs without worries.

Stubbing

To generate stubs based on a ContractList you can run:

contracts = Pacto.load_contracts('contracts/services', 'http://example.com')
contracts.stub_providers

This method uses webmock to ensure that whenever you make a request against one of contracts you actually get a stubbed response, based on the default values specified on the contract, instead of hitting the real provider.

You can override any default value on the contracts by providing a hash of optional values to the stub_providers method. This will override the keys for every contract in the list

contracts = Pacto.load_contracts('contracts/services', 'http://example.com')
contracts.stub_providers(request_id: 14, name: "Marcos")

Pacto Server (non-Ruby usage)

See also: http://thoughtworks.github.io/pacto/patterns/polyglot/

We've bundled a small server that embeds pacto so you can use it for non-Ruby projects. If you want to take advantage of the full features, you'll still need to use Ruby (usually rspec) to drive your API testing. You can run just the server in order to stub or to write validation issues to a log, but you won't have access to the full API fail your tests if there were validation problems.

Command-line

The command-line version of the server will show you all the options. These same options are used when you launch the server from within rspec. In order to see the options: bundle exec pacto-server --help

Some examples:

$ bundle exec pacto-server -sv --generate
# Runs a server that will generate Contracts for each request received
$ bundle exec pacto-server -sv --stub --validate
# Runs the server that provides stubs and checks them against Contracts
$ bundle exec pacto-server -sv --live --validate --host
# Runs the server that acts as a proxy to http://example.com, validating each request/response against a Contract

RSpec test helper

You can also launch a server from within an rspec test. The server does start up an external port and runs asynchronously so it doens't block your main test thread from kicking off your consumer code. This gives you an externally accessable server that non-Ruby clients can hit, but still gives you the full Pacto API in order to validate and spy on HTTP interactions.

Example usage of the rspec test helper:

require 'rspec'
require 'pacto/rspec'
require 'pacto/test_helper'

describe 'my consumer' do
  include Pacto::TestHelper

  it 'calls a service' do
    with_pacto(
      :port => 5000,
      :directory => '../spec/integration/data',
      :stub => true) do |pacto_endpoint|
      # call your code
      system "curl #{pacto_endpoint}/echo"
      # check results
      expect(Pacto).to have_validated(:get, 'https://localhost/echo')
    end
  end
end

Rake Tasks

Pacto includes a few Rake tasks to help with common tasks. If you want to use these tasks, just add this top your Rakefile:

require 'pacto/rake_task'

This should add several new tasks to you project:

rake pacto:generate[input_dir,output_dir,host]  # Generates contracts from partial contracts
rake pacto:meta_validate[dir]                   # Validates a directory of contract definitions
rake pacto:validate[host,dir]                   # Validates all contracts in a given directory against a given host

The pacto:generate task will take partially defined Contracts and generate the missing pieces. See Generate for more details.

The pacto:meta_validate task makes sure that your Contracts are valid. It only checks the Contracts, not the services that implement them.

The pacto:validate task sends a request to an actual provider and ensures their response complies with the Contract.

Contracts

Pacto works by associating a service with a Contract. The Contract is a JSON description of the service that uses json-schema to describe the response body. You don't need to write your contracts by hand. In fact, we recommend generating a Contract from your documentation or a service.

A contract is composed of a request that has:

  • Method: the method of the HTTP request (e.g. GET, POST, PUT, DELETE);
  • Path: the relative path (without host) of the provider's endpoint;
  • Headers: headers sent in the HTTP request;
  • Params: any data or parameters of the HTTP request (e.g. query string for GET, body for POST).

And a response has that has:

  • Status: the HTTP response status code (e.g. 200, 404, 500);
  • Headers: the HTTP response headers;
  • Body: a JSON Schema defining the expected structure of the HTTP response body.

Below is an example contract for a GET request to the /hello_world endpoint of a provider:

{
  "request": {
    "method": "GET",
    "path": "/hello_world",
    "headers": {
      "Accept": "application/json"
    },
    "params": {}
  },

  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "description": "A simple response",
      "type": "object",
      "properties": {
        "message": {
          "type": "string"
        }
      }
    }
  }
}

Constraints

  • Pacto only works with JSON services
  • Pacto requires Ruby 1.9.3 or newer (though you can older Rubies or non-Ruby projects with a Pacto Server)
  • Pacto cannot currently specify multiple acceptable status codes (e.g. 200 or 201)

Contributing

Read the CONTRIBUTING.md file.