This simple client/server implementation demos how pact-python can be used for a contract test.
Further reading about pact is at https://docs.pact.io/ or https://docs.pact.io/blogs-videos-and-articles
It has 3 components:
pact_python_demo/user-app.py
-- a simple flask app that has a REST endpoint for/users/<name>
which returns a JSON representation of a userpact_python_demo/client.py
-- a simple client that gets a user from user-app.tests/test_client.py
-- a set of test cases using pytest and pact-python to test a simple contract between the client and server.broker/
-- contains docker-compose files for a pact broker server
Set up your virtual environment with:
$ pip install pipenv
$ pipenv install
Enter your venv shell with:
$ pipenv shell
Run the test with:
(venv) $ pytest
You'll see a pact file is generated by this consumer test at tests/userserviceclient-userservice.json
Then, fire up your server-side app and verify the provider works as expected:
(venv) $ python pact_python_demo/user-app.py
(venv) $ pact-verifier --provider-base-url=http://localhost:5001 \
--pact-url=tests/userserviceclient-userservice.json \
--provider-states-setup-url=http://localhost:5001/_pact/provider_states
Next, you can try incorporating the pact broker in this process:
Start the broker server:
$ cd broker
$ docker-compose up
It's accessible at http://127.0.0.1 with username 'pactbroker' and password 'pactbroker'
To run the test, this time pushing the generated pact into the broker with version '0.1' of UserServiceClient, use:
(venv) $ pytest --publish-pact 0.1
The tests/conftest.py
adds this custom option, and tests/test_client
has code in the pact
fixture to upload to the broker if the command line option was passed.
Then, you can validate the provider (UserService) and push the result to the broker by running:
(venv) $ python pact_python_demo/user-app.py
(venv) ./verify_pact.sh 0.2
This runs the same pact-verifier
command as above but has additional args for pulling the pact file from the broker (instead of getting the .json locally) and pushing the verification result to the broker.
Log in to the broker and take a look at the matrix. You will see that UserClientService version 0.1 has been verified against UserService version 0.2
The purpose of test_client.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.
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
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
.
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.
A key part of this is provider states:
client.py
expects a 200OK response with json about 'UserA' given that userA exists on the provider- 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 aprovider-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) - In this demo, the user app has provided another REST endpoint at
/_pact/provider_states
which thepact-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.
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>.
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"