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

Commit

Permalink
Merge ef436c6 into b061db5
Browse files Browse the repository at this point in the history
  • Loading branch information
maxlinc committed Jul 8, 2014
2 parents b061db5 + ef436c6 commit acf45d5
Show file tree
Hide file tree
Showing 29 changed files with 1,081 additions and 45 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ RSpec/DescribeClass:
Exclude:
- samples/**/*
- spec/integration/**/*
RSpec/MultipleDescribes:
Exclude:
- samples/**/*
1 change: 1 addition & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ desc 'Run the samples'
task :samples do
FileUtils.rm_rf('samples/tmp')
sh 'bundle exec polytrix exec --code2doc samples/*.rb samples/*.sh'
sh 'bundle exec polytrix exec --code2doc samples/*.json --lang js'
end

desc 'Build the documentation from the samples'
Expand Down
58 changes: 58 additions & 0 deletions docs/actors.md
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"}
```
40 changes: 40 additions & 0 deletions docs/clerks.md
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}"
```

39 changes: 39 additions & 0 deletions docs/configuration.md
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
```

109 changes: 109 additions & 0 deletions docs/contracts.md
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
}
}
}
}
}
}
}
```

50 changes: 50 additions & 0 deletions docs/cops.md
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
```

64 changes: 64 additions & 0 deletions docs/forensics.md
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
```

Loading

0 comments on commit acf45d5

Please sign in to comment.