This repository has been archived by the owner on Dec 5, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
29 changed files
with
1,081 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
Pacto uses actors stub **providers** or simulate **consumers** based on the contract. There | ||
are two built-in actors. The FromExamples actor will produce requests or responses based on | ||
the examples in the contract. The JSONGenerator actor will attempt to generate requests or | ||
responses that match the JSON schema, though it only works for simple schemas. The FromExamples | ||
actor is the default, but falls back to the JSONGenerator actor if there are no examples available. | ||
Consider the following contract: | ||
|
||
```json | ||
{ | ||
"name": "Ping", | ||
"request": { | ||
"headers": { | ||
}, | ||
"http_method": "get", | ||
"path": "/api/ping" | ||
}, | ||
"response": { | ||
"headers": { | ||
"Content-Type": "application/json" | ||
}, | ||
"status": 200, | ||
"schema": { | ||
"$schema": "http://json-schema.org/draft-03/schema#", | ||
"type": "object", | ||
"required": true, | ||
"properties": { | ||
"ping": { | ||
"type": "string", | ||
"required": true | ||
} | ||
} | ||
} | ||
}, | ||
"examples": { | ||
"default": { | ||
"request": { | ||
}, | ||
"response": { | ||
"body": { | ||
"ping": "pong - from the example!" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
Then Pacto will generate the following response by default (via FromExamples): | ||
|
||
```json | ||
{"ping":"pong - from the example!"} | ||
``` | ||
|
||
If you didn't have an example, then Pacto generate very basic mock data based on the schema types, | ||
producing something like: | ||
|
||
```json | ||
{"ping":"bar"} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
Pacto clerks are responsible for loading contracts. Pacto has built-in support for a native | ||
contract format, but we've setup a Clerks plugin API so you can more easily load information | ||
from other formats, like [Swagger](https://github.com/wordnik/swagger-spec), | ||
[apiblueprint](http://apiblueprint.org/), or [RAML](http://raml.org/). | ||
Note: This is a preliminary API and may change in the future, including adding support | ||
for conversion between formats, or generating each format from real HTTP interactions. | ||
In order to add a loading clerk, you just implement a class that responds to build_from_file | ||
and returns Contract object or a collection of Contract objects. | ||
|
||
```rb | ||
require 'yaml' | ||
require 'pacto' | ||
|
||
class SimpleYAMLClerk | ||
def build_from_file(path, _host) | ||
data = YAML.load(File.read(path)) | ||
data['services'].map do | service_name, service_definition | | ||
request_clause = Pacto::RequestClause.new service_definition['request'] | ||
response_clause = Pacto::ResponseClause.new service_definition['response'] | ||
Pacto::Contract.new(name: service_name, request: request_clause, response: response_clause) | ||
end | ||
end | ||
end | ||
``` | ||
|
||
You can then register the clerk with Pacto: | ||
|
||
```rb | ||
Pacto.contract_factory.add_factory :simple_yaml, SimpleYAMLClerk.new | ||
``` | ||
|
||
And then you can use it with the normal clerks API, by passing the identifier you used to register | ||
the clerk: | ||
|
||
```rb | ||
contracts = Pacto.load_contracts 'simple_service_map.yaml', 'http://example.com', :simple_yaml | ||
contract_names = contracts.map(&:name) | ||
puts "Defined contracts: #{contract_names}" | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
Pacto will disable live connections, so you will get an error if | ||
your code unexpectedly calls an service that was not stubbed. If you | ||
want to re-enable connections, run `WebMock.allow_net_connect!` after | ||
requiring pacto. | ||
|
||
```rb | ||
require 'pacto' | ||
WebMock.allow_net_connect! | ||
``` | ||
|
||
Pacto can be configured via a block: | ||
|
||
```rb | ||
Pacto.configure do |c| | ||
c.contracts_path = 'contracts' # Path for loading/storing contracts. | ||
c.strict_matchers = true # If the request matching should be strict (especially regarding HTTP Headers). | ||
c.stenographer_log_file = nil # Set to nil to disable the stenographer log. | ||
end | ||
``` | ||
|
||
You can also do inline configuration. This example tells the | ||
[json-schema-generator](https://github.com/maxlinc/json-schema-generator) to | ||
store default values in the schema. | ||
|
||
```rb | ||
Pacto.configuration.generator_options = { defaults: true } | ||
``` | ||
|
||
All Pacto configuration and metrics can be reset ia `Pacto.clear!`. If you're using | ||
RSpec you may want to clear between each scenario: | ||
If you're using Pacto's rspec matchers you might want to configure a reset between each scenario | ||
|
||
```rb | ||
require 'pacto/rspec' | ||
RSpec.configure do |c| | ||
c.after(:each) { Pacto.clear! } | ||
end | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
Pacto Contracts describe the constraints we want to put on interactions between a consumer and a provider. It sets some expectations about the headers expected for both the request and response, the expected response status code. It also uses [json-schema](http://json-schema.org/) to define the allowable request body (if one should exist) and response body. | ||
|
||
```js | ||
{ | ||
``` | ||
The Request section comes first. In this case, we're just describing a simple get request that does not require any parameters or a request body. | ||
```js | ||
"request": { | ||
"headers": { | ||
``` | ||
A request must exactly match these headers for Pacto to believe the request matches the contract, unless `Pacto.configuration.strict_matchers` is false. | ||
```js | ||
"Accept": "application/vnd.github.beta+json", | ||
"Accept-Encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" | ||
}, | ||
``` | ||
The `method` and `path` are required. The `path` may be an [rfc6570 URI template](http://tools.ietf.org/html/rfc6570) for more flexible matching. | ||
```js | ||
"http_method": "get", | ||
"path": "/repos/thoughtworks/pacto/readme" | ||
}, | ||
"response": { | ||
"headers": { | ||
"Content-Type": "application/json; charset=utf-8", | ||
"Status": "200 OK", | ||
"Cache-Control": "public, max-age=60, s-maxage=60", | ||
"Etag": "\"fc8e78b0a9694de66d47317768b20820\"", | ||
"Vary": "Accept, Accept-Encoding", | ||
"Access-Control-Allow-Credentials": "true", | ||
"Access-Control-Expose-Headers": "ETag, Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", | ||
"Access-Control-Allow-Origin": "*" | ||
}, | ||
"status": 200, | ||
"schema": { | ||
"$schema": "http://json-schema.org/draft-03/schema#", | ||
"description": "Generated from https://api.github.com/repos/thoughtworks/pacto/readme with shasum 3ae59164c6d9f84c0a81f21fb63e17b3b8ce6894", | ||
"type": "object", | ||
"required": true, | ||
"properties": { | ||
"name": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"path": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"sha": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"size": { | ||
"type": "integer", | ||
"required": true | ||
}, | ||
"url": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"html_url": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"git_url": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"type": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"content": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"encoding": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"_links": { | ||
"type": "object", | ||
"required": true, | ||
"properties": { | ||
"self": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"git": { | ||
"type": "string", | ||
"required": true | ||
}, | ||
"html": { | ||
"type": "string", | ||
"required": true | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
You can create a custom cop that investigates the request/response and sees if it complies with a | ||
contract. The cop should return a list of citations if it finds any problems. | ||
|
||
```rb | ||
require 'pacto' | ||
class MyCustomCop | ||
def investigate(_request, _response, contract) | ||
citations = [] | ||
citations << 'Contract must have a request schema' if contract.request.schema.empty? | ||
citations << 'Contract must have a response schema' if contract.response.schema.empty? | ||
citations | ||
end | ||
end | ||
``` | ||
|
||
You can activate the cop by adding it to the active_cops. The active_cops are reset | ||
by `Pacto.clear!` | ||
|
||
```rb | ||
Pacto::Cops.active_cops << MyCustomCop.new | ||
``` | ||
|
||
Or you could add it as a registered cop. These cops are not cleared - they form the | ||
default set of Cops used by Pacto: | ||
|
||
```rb | ||
Pacto::Cops.register_cop MyCustomCop.new | ||
``` | ||
|
||
The cops will be used to validate any service requests/responses detected by Pacto, | ||
including when we simulate consumers: | ||
|
||
```rb | ||
Pacto.validate! | ||
contracts = Pacto.load_contracts('contracts', 'http://localhost:5000') | ||
contracts.stub_providers | ||
puts contracts.simulate_consumers | ||
``` | ||
|
||
You could also completely reset the registered cops if you don't want to use | ||
all of Pacto's built-in cops: | ||
|
||
```rb | ||
Pacto::Cops.registered_cops.clear | ||
Pacto::Cops.register_cop Pacto::Cops::ResponseBodyCop | ||
|
||
contracts = Pacto.load_contracts('contracts', 'http://localhost:5000') | ||
puts contracts.simulate_consumers | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
Pacto has a few RSpec matchers to help you ensure a **consumer** and **producer** are | ||
interacting properly. First, let's setup the rspec suite. | ||
|
||
```rb | ||
require 'rspec/autorun' # Not generally needed | ||
require 'pacto/rspec' | ||
WebMock.allow_net_connect! | ||
Pacto.validate! | ||
Pacto.load_contracts('contracts', 'http://localhost:5000').stub_providers | ||
``` | ||
|
||
It's usually a good idea to reset Pacto between each scenario. `Pacto.reset` just clears the | ||
data and metrics about which services were called. `Pacto.clear!` also resets all configuration | ||
and plugins. | ||
|
||
```rb | ||
RSpec.configure do |c| | ||
c.after(:each) { Pacto.reset } | ||
end | ||
``` | ||
|
||
Pacto provides some RSpec matchers related to contract testing, like making sure | ||
Pacto didn't received any unrecognized requests (`have_unmatched_requests`) and that | ||
the HTTP requests matched up with the terms of the contract (`have_failed_investigations`). | ||
|
||
```rb | ||
describe Faraday do | ||
let(:connection) { described_class.new(url: 'http://localhost:5000') } | ||
|
||
it 'passes contract tests' do | ||
connection.get '/api/ping' | ||
expect(Pacto).to_not have_failed_investigations | ||
expect(Pacto).to_not have_unmatched_requests | ||
end | ||
end | ||
``` | ||
|
||
There are also some matchers for collaboration testing, so you can make sure each scenario is | ||
calling the expected services and sending the right type of data. | ||
|
||
```rb | ||
describe Faraday do | ||
let(:connection) { described_class.new(url: 'http://localhost:5000') } | ||
before(:each) do | ||
connection.get '/api/ping' | ||
|
||
connection.post do |req| | ||
req.url '/api/echo' | ||
req.headers['Content-Type'] = 'application/json' | ||
req.body = '{"foo": "bar"}' | ||
end | ||
end | ||
|
||
it 'calls the ping service' do | ||
expect(Pacto).to have_validated(:get, 'http://localhost:5000/api/ping').against_contract('Ping') | ||
end | ||
|
||
it 'sends data to the echo service' do | ||
expect(Pacto).to have_investigated('Echo').with_request(body: hash_including('foo' => 'bar')) | ||
expect(Pacto).to have_investigated('Echo').with_response(body: hash_including('foo' => 'bar')) | ||
end | ||
end | ||
``` | ||
|
Oops, something went wrong.