Skip to content

Commit 1d023b0

Browse files
committed
Initial commit
1 parent 97fae67 commit 1d023b0

File tree

8 files changed

+446
-0
lines changed

8 files changed

+446
-0
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
*.json
2+
log/*
3+
reports/*
4+
tmp/*
5+
*.pyc
6+
*.log
7+
.cache/*
8+
.pytest_cache/*

Pipfile

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[dev-packages]
7+
8+
[packages]
9+
atomicwrites = "*"
10+
attrs = "*"
11+
certifi = "*"
12+
chardet = "*"
13+
click = "*"
14+
idna = "*"
15+
more-itertools = "*"
16+
pact-python = "*"
17+
pipenv = "*"
18+
pluggy = "*"
19+
psutil = "*"
20+
py = "*"
21+
pytest = "*"
22+
requests = "*"
23+
six = "*"
24+
"urllib3" = "*"
25+
virtualenv = "*"
26+
virtualenv-clone = "*"
27+
flask = "*"
28+
29+
[requires]
30+
python_version = "2.7"

Pipfile.lock

+225
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
pact-python demo
2+
================
3+
4+
This simple client/server implementation demos how pact-python can be used for a contract test.
5+
6+
Further reading about pact is at https://docs.pact.io/ or https://docs.pact.io/blogs-videos-and-articles
7+
8+
It has 3 components:
9+
* `user-app.py` -- a simple flask app that has a REST endpoint for `/users/<name>` which returns a JSON representation of a user
10+
* `client.py` -- a simple client that gets a user from user-app.
11+
* `client-test.py` -- a set of test cases using pytest and pact-python to test a simple
12+
contract between the client and server.
13+
14+
Install this with:
15+
16+
```
17+
$ pip install pipenv
18+
$ pipenv install
19+
```
20+
21+
Enter your venv shell with:
22+
```
23+
$ pipenv shell
24+
```
25+
26+
Run the test with:
27+
```
28+
(venv) $ pytest client-test.py
29+
```
30+
31+
You'll see a pact file is generated by this consumer test at `consumer-provider.json`
32+
33+
Then, fire up your server-side app and verify the provider works as expected:
34+
```
35+
(venv) $ python user-app.py
36+
(venv) $ pact-verifier --provider-base-url=http://localhost:5001 \
37+
--pact-url=./consumer-provider.json \
38+
--provider-states-setup-url=http://localhost:5001/_pact/provider_states
39+
```
40+
41+
How does this work?
42+
===================
43+
44+
The purpose of `client-test.py` is to simply verify that **if the server sends me what I'm expecting, MY client code behaves properly**. It is essentially a unit test to exercise your client code using a mocked server. Except in this case, the mock server is now a "pact mock provider". In the test, you have configured the mock provider server to respond to your client with certain data. This is the mock data to represent how you'd expect the provider to behave **given the proper state exists on the provider.** Using that mock data, you verify your client-side code works.
45+
46+
Two tests were created to verify that my client behaves properly if: a) I get a 200OK response back w/ the json I'd expect to see, or b) I get a 404 back and I should be returning `None`
47+
48+
For example, `client` expects that if `UserA` exists and it sends a GET to `/users/UserA`, it will get back JSON about UserA that has a name, uuid, timestamp for when the user was created, and a boolean indicating if the users is an admin or not. If the server responds with a 404, user client should return `None`.
49+
50+
When your tests pass, a json **pact file** is generated. The `user-app` team can then take this pact file, and use it to verify that their service responds to requests in the way your client expects.
51+
52+
## Provider states
53+
54+
A key part of this is **provider states**:
55+
* `client.py` expects a 200OK response with json about 'UserA' **given that userA exists on the provider**
56+
* In order for pact to verify the provider, `user-app.py` has to implement ways of **setting up the needed state**. This is done via a `provider-setup-url` which may reside on the provider app itself, or it could reside somewhere else (for example, some other test service you have setup that lives outside your provider app to populate your backend DB)
57+
* In this demo, the user app has provided another REST endpoint at `/_pact/provider_states` which the `pact-verifier` will send POST requests to. Depending on the state the pact-verifier asks for, the provider will need to set itself up (e.g. make DB changes, or do other things) to get itself into "the right state" before the verifier sends the "request under test" to it.
58+
59+
## Isn't my server gonna send back different stuff?
60+
Yes. The real server may respond with a timestamp or uuid that does not necessarily look like the mock values you created in your tests. Therefore, the `Term` object is used to say that **the server could send me anything that looks like this regex and that would be good for me, but for the purpose of this test I am using \<this fake value\>**.
61+
62+
The generated pact file used to verify the provider will only say "you need to be sending me data that looks like this regex" for timestamp and id. But, my client is not going to let server return with `name=UserB` if it asked for `name=UserA`, so the pact file is essentially saying "given that User A exists, and I send you a GET for userA, `name` in the JSON should be UserA"

__init__.py

Whitespace-only changes.

client-test.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import atexit
2+
import os
3+
import unittest
4+
5+
from pact import Consumer, Like, Provider, Term
6+
import pytest
7+
8+
from .client import UserClient
9+
10+
PACT_MOCK_HOST = 'localhost'
11+
PACT_MOCK_PORT = 1234
12+
13+
14+
@pytest.fixture
15+
def client():
16+
return UserClient(
17+
'http://{host}:{port}'
18+
.format(host=PACT_MOCK_HOST, port=PACT_MOCK_PORT)
19+
)
20+
21+
22+
@pytest.fixture(scope='module')
23+
def pact():
24+
pact = Consumer('Consumer').has_pact_with(
25+
Provider('Provider'), host_name=pact_mock_host, port=pact_mock_port)
26+
pact.start_service()
27+
atexit.register(pact.stop_service)
28+
return pact
29+
30+
31+
def test_get_user_non_admin(pact, client):
32+
expected = {
33+
'name': 'UserA',
34+
'id': Term(
35+
r'^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}\Z',
36+
'00000000-0000-4000-a000-000000000000'
37+
),
38+
'created_on': Term(
39+
r'\d+-\d+-\d+T\d+:\d+:\d+',
40+
'2016-12-15T20:16:01'
41+
),
42+
'admin': False
43+
}
44+
45+
(pact
46+
.given('UserA exists and is not an administrator')
47+
.upon_receiving('a request for UserA')
48+
.with_request('get', '/users/UserA')
49+
.will_respond_with(200, body=Like(expected)))
50+
51+
with pact:
52+
result = client.get_user('UserA')
53+
54+
# assert something with the result, for ex, did I process 'result' properly?
55+
# or was I able to deserialize correctly? etc.
56+
57+
def test_get_non_existing_user(pact, client):
58+
(pact
59+
.given('UserA does not exist')
60+
.upon_receiving('a request for UserA')
61+
.with_request('get', '/users/UserA')
62+
.will_respond_with(404))
63+
64+
with pact:
65+
result = client.get_user('UserA')
66+
67+
assert result is None

0 commit comments

Comments
 (0)