Skip to content

Commit 8f074a0

Browse files
Merge pull request #1 from pact-foundation/initial-framework
Initial pact-python implementation
2 parents 3949b54 + f5caf9c commit 8f074a0

24 files changed

+1621
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# pact-python specific ignores
2+
e2e/pacts
3+
14
# Byte-compiled / optimized / DLL files
25
__pycache__/
36
*.py[cod]

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v0.1.0, 2017-03-30 -- Basic support for authoring contracts against a separately running mock service

MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include LICENSE
2+
include *.txt
3+
include *.md
4+
prune *test

Makefile

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
DOCS_DIR := ./docs
2+
3+
define VERSION_CHECK
4+
import setup
5+
import pact
6+
msg = 'pact.__version__ must match the last version in CHANGES.txt'
7+
assert setup.get_version() == pact.__version__, msg
8+
endef
9+
10+
11+
help:
12+
@echo ""
13+
@echo " deps to install the required files for development"
14+
@echo " package to create a distribution package in /dist/"
15+
@echo " release to perform a release build, including deps, test, and package targets"
16+
@echo " test to run all tests"
17+
@echo ""
18+
19+
20+
.PHONY: release
21+
release: deps test package
22+
23+
24+
.PHONY: deps
25+
deps:
26+
pip install -r requirements_dev.txt
27+
28+
29+
.PHONY: e2e
30+
e2e:
31+
sh -c '\
32+
cd e2e; \
33+
docker-compose pull > /dev/null; \
34+
docker-compose up -d pactmockservice; \
35+
while ! nc -z localhost 1234; do sleep 0.1; done; \
36+
docker-compose logs --follow > ./pacts/mock-service-logs.txt & \
37+
nosetests ./contracts; \
38+
docker-compose down; \
39+
docker-compose up -d app pactverifier; \
40+
docker-compose logs --follow >> ./pact/verifier-logs.txt & \
41+
docker-compose exec pactverifier bundle exec rake verify_pacts; \
42+
docker-compose down'
43+
44+
.PHONY: package
45+
package:
46+
python setup.py sdist
47+
48+
49+
export VERSION_CHECK
50+
.PHONY: test
51+
test: deps
52+
@echo "Checking version consistency..."
53+
python -c "$$VERSION_CHECK"
54+
55+
@echo "flake8..."
56+
flake8
57+
58+
@echo "pydocstyle..."
59+
pydocstyle pact
60+
61+
@echo "testing..."
62+
tox

README.md

Lines changed: 296 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,297 @@
11
# pact-python
2-
Python version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
2+
Python version of Pact. Enables consumer driven contract testing,
3+
providing a mock service and DSL for the consumer project, and
4+
interaction playback and verification for the service provider project.
5+
6+
For more information about what Pact is, and how it can help you
7+
test your code more efficiently, check out the [Pact documentation].
8+
9+
# How to use pact-python
10+
11+
## Installation
12+
```
13+
pip install pact-python
14+
```
15+
16+
## Writing a Pact
17+
Creating a complete contract is a two step process:
18+
19+
1. Create a test on the consumer side that declares the expectations it has of the provider
20+
2. Create a provider state that allows the contract to pass when replayed against the provider
21+
22+
## Writing the Consumer Test
23+
24+
If we have a method that communicates with one of our external services, which we'll call
25+
`Provider`, and our product, `Consumer` is hitting an endpoint on `Provider` at
26+
`/users/<user>` to get information about a particular user.
27+
28+
If the code to fetch a user looked like this:
29+
30+
```python
31+
import requests
32+
33+
34+
def user(user_name):
35+
"""Fetch a user object by user_name from the server."""
36+
uri = 'http://localhost:1234/users/' + user_name
37+
return requests.get(uri).json()
38+
```
39+
40+
Then `Consumer`'s contract test might look something like this:
41+
42+
```python
43+
import unittest
44+
45+
from pact import Consumer, Provider
46+
47+
48+
class GetUserInfoContract(unittest.TestCase):
49+
def test_get_user(self):
50+
pact = Consumer('Consumer').has_pact_with(Provider('Provider'))
51+
expected = {
52+
'username': 'UserA',
53+
'id': 123,
54+
'groups': ['Editors']
55+
}
56+
57+
(pact
58+
.given('UserA exists and is not an administrator')
59+
.upon_receiving('a request for UserA')
60+
.with_request('get', '/users/UserA')
61+
.will_respond_with(200, body=expected))
62+
63+
with pact:
64+
result = user('UserA')
65+
66+
self.assertEqual(result, expected)
67+
68+
```
69+
70+
This does a few important things:
71+
72+
- Defines the Consumer and Provider objects that describe our product and our service under test
73+
- Uses `given` to define the setup criteria for the Provider `UserA exists and is not an administrator`
74+
- Defines what the request that is expected to be made by the consumer will contain
75+
- Defines how the server is expected to respond
76+
77+
Using the Pact object as a [context manager], we call our method under test
78+
which will then communicate with the Pact mock service. The mock service will respond with
79+
the items we defined, allowing us to assert that the method processed the response and
80+
returned the expected value.
81+
82+
The default hostname and port for the Pact mock service will be
83+
`localhost:1234` but you can adjust this during Pact creation:
84+
85+
```python
86+
from pact import Consumer, Provider
87+
pact = Consumer('Consumer').has_pact_with(
88+
Provider('Provider'), host_name='mockservice', port=8080)
89+
```
90+
91+
This can be useful if you are running your tests and the mock service inside a Docker
92+
network, where you want to reference the service by its Docker name, instead of via
93+
the `localhost` interface. It is important to note that the code you are testing with
94+
this contract _must_ contact the mock service. So in this example, the `user` method
95+
could accept an argument to specify the location of the server, or retrieve it from an
96+
environment variable so you can change its URI during the test. Another option is to
97+
specify a Docker network alias so the requests that you make will go to the container.
98+
99+
The mock service offers you several important features when building your contracts:
100+
- It provides a real HTTP server that your code can contact during the test and provides the responses you defined.
101+
- You provide it with the expectations for the request your code will make and it will assert the contents of the actual requests made based on your expectations.
102+
- If a request is made that does not match one you defined or if a request from your code is missing it will return an error with details.
103+
- Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker.
104+
105+
## Expecting Variable Content
106+
The above test works great if that user information is always static, but what happens if
107+
the user has a last updated field that is set to the current time every time the object is
108+
modified? To handle variable data and make your tests more robust, there are 3 helpful matchers:
109+
110+
### Term(matcher, generate)
111+
Asserts the value should match the given regular expression. You could use this
112+
to expect a timestamp with a particular format in the request or response where
113+
you know you need a particular format, but are unconcerned about the exact date:
114+
115+
```python
116+
from pact import Term
117+
...
118+
body = {
119+
'username': 'UserA',
120+
'last_modified': Term('\d+-\d+-\d+T\d+:\d+:\d+', '2016-12-15T20:16:01')
121+
}
122+
123+
(pact
124+
.given('UserA exists and is not an administrator')
125+
.upon_receiving('a request for UserA')
126+
.with_request('get', '/users/UserA/info')
127+
.will_respond_with(200, body=body))
128+
```
129+
130+
When you run the tests for the consumer, the mock service will return the value you provided
131+
as `generate`, in this case `2016-12-15T20:16:01`. When the contract is verified on the
132+
provider, the regex will be used to search the response from the real provider service
133+
and the test will be considered successful if the regex finds a match in the response.
134+
135+
### SomethingLike(matcher)
136+
Asserts the element's type matches the matcher. For example:
137+
138+
```python
139+
from pact import SomethingLike
140+
SomethingLike(123) # Matches if the value is an integer
141+
SomethingLike('hello world') # Matches if the value is a string
142+
SomethingLike(3.14) # Matches if the value is a float
143+
```
144+
145+
The argument supplied to `SomethingLike` will be what the mock service responds with.
146+
147+
### EachLike(matcher, minimum=None, maximum=None)
148+
Asserts the value is an array type that consists of elements
149+
like the ones passed in. It can be used to assert simple arrays:
150+
151+
```python
152+
from pact import EachLike
153+
EachLike(1) # All items are integers
154+
EachLike('hello') # All items are strings
155+
```
156+
157+
Or other matchers can be nested inside to assert more complex objects:
158+
159+
```python
160+
from pact import EachLike, SomethingLike, Term
161+
EachLike({
162+
'username': Term('[a-zA-Z]+', 'username'),
163+
'id': SomethingLike(123),
164+
'groups': EachLike('administrators')
165+
})
166+
```
167+
168+
> Note, you do not need to specify everything that will be returned from the Provider in a
169+
> JSON response, any extra data that is received will be ignored and the tests will still pass.
170+
171+
For more information see [Matching](https://docs.pact.io/documentation/matching.html)
172+
173+
## Running the Mock Service
174+
This library does not yet automatically handle running the [Pact Mock Service] so you will need to
175+
start that manually before running the tests. There are two primary ways to run the mock service:
176+
177+
1. [Install it and run it using Ruby](https://github.com/bethesque/pact-mock_service#usage)
178+
2. Run it as a Docker container
179+
180+
Using the Docker container additionally has two options. You can run it via the `docker` command:
181+
182+
```
183+
docker run -d -p "1234:1234" -v /tmp/log:/var/log/pacto -v $(pwd)/contracts:/opt/contracts madkom/pact-mock-service
184+
```
185+
186+
Which will start the service and expose it as port `1234` on your computer, mount
187+
`/tmp/log` on your machine to house the mock service log files, and the directory
188+
`contracts` in the current working directory to house the contracts when they are published.
189+
190+
Additionally, you could run the mock service using `docker-compose`:
191+
192+
```yaml
193+
version: '2'
194+
services:
195+
pactmockservice:
196+
image: madkom/pact-mock-service
197+
ports:
198+
- "1234:1234"
199+
volumes:
200+
- /tmp/pact:/var/log/pacto
201+
- ./contracts:/opt/contracts
202+
```
203+
204+
> Note: How you run the mock service may change what hostname and port you should
205+
> use when running your consumer tests. For example: If you change the host port on
206+
> the command line to be `8080`, your tests would need to contact `localhost:8080`.
207+
208+
## Verifying Pacts Against a Service
209+
> pact-python does not yet have any involvement in the process of verifying a contract against
210+
> a provider. This section is included to provide insight into the full cycle of a
211+
> contract for those getting started.
212+
213+
Like the mock service, the provider verifier can be run in two ways:
214+
215+
1. [Install and use it as a Ruby application][pact-provider-verifier]
216+
2. Run it as a Docker container
217+
218+
> Both choices have very similar configuration options. We will illustrate the Docker
219+
> method below, but the Ruby method supports the same features.
220+
221+
When verifying your contracts, you may find it easier to run the provider application
222+
and the verifier in separate Docker containers. This gives you a nice isolated
223+
network, where you can set the DNS records of the services to anything you desire
224+
and not have to worry about port conflicts with other services on your computer.
225+
Launching the provider verifier in a `docker-compose.yml` might look like this:
226+
227+
```yaml
228+
version: '2'
229+
services:
230+
app:
231+
image: the-provider-application-to-test
232+
233+
pactverifier:
234+
command: ['tail', '-f', '/dev/null']
235+
image: dius/pact-provider-verifier-docker
236+
depends_on:
237+
- app
238+
volumes:
239+
- ./contracts:/tmp/pacts
240+
environment:
241+
- pact_urls=/tmp/pacts/consumer-provider.json
242+
- provider_base_url=http://app
243+
- provider_states_url=http://app/_pact/provider-states
244+
- provider_states_active_url=http://app/_pact/provider-states/active
245+
```
246+
247+
In this example, our `app` container may take a few moments to start, so we don't
248+
immediately start running the verification, and instead `tail -f /dev/null` which will keep
249+
the container running forever. We can then use `docker-compose` to run the tests like so:
250+
251+
```
252+
docker-compose up -d
253+
# Insert code to check that `app` has finished starting and is ready for requests
254+
docker-compose exec pactverifier bundle exec rake verify_pacts
255+
```
256+
257+
You configure the verifier in Docker using 4 environment variables:
258+
- `pact_urls` - a comma delimited list of pact file urls
259+
- `provider_base_url` - the base url of the pact provider
260+
- `provider_states_url` - the full url of the endpoint which returns provider states by consumer
261+
- `provider_states_active_url` - the full url of the endpoint which sets the active pact consumer and provider state
262+
263+
### Provider States
264+
In many cases, your contracts will need very specific data to exist on the provider
265+
to pass successfully. If you are fetching a user profile, that user needs to exist,
266+
if querying a list of records, one or more records needs to exist. To support
267+
decoupling the testing of the consumer and provider, Pact offers the idea of provider
268+
states to communicate from the consumer what data should exist on the provider.
269+
270+
When setting up the testing of a provider you will also need to setup the management of
271+
these provider states. The Pact verifier does this by making additional HTTP requests to
272+
the `provider_states_url` and `provider_stats_active_url` you provide. These URLs could be
273+
on the provider application or a separate one. Some strategies for managing state include:
274+
275+
- Having endpoints in your application that are not active in production that create and delete your datastore state
276+
- A separate application that has access to the same datastore to create and delete, like a separate App Engine module or Docker container pointing to the same datastore
277+
- A standalone application that can start and stop the other server with different datastore states
278+
279+
For more information about provider states, refer to the [Pact documentation] on [Provider States].
280+
281+
# Development
282+
Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md)
283+
284+
Create a Python virtualenv for use with this project
285+
`make release`
286+
287+
## Testing
288+
Unit: `make test`
289+
290+
End to end: `make e2e`
291+
292+
[context manager]: https://en.wikibooks.org/wiki/Python_Programming/Context_Managers
293+
[Pact]: https://www.gitbook.com/book/pact-foundation/pact/details
294+
[Pact documentation]: https://docs.pact.io/
295+
[Pact Mock Service]: https://github.com/bethesque/pact-mock_service
296+
[Provider States]: https://docs.pact.io/documentation/provider_states.html
297+
[pact-provider-verifier]: https://github.com/pact-foundation/pact-provider-verifier

e2e/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)