Python version of Pact mocking, generation and verification.
Enables consumer driven contract testing, providing unit test mocking of provider services and DSL for the consumer project, and interaction playback and verification for the service provider project. Currently supports versions 1.1, 2 and 3 of the Pact specification.
For more information about what Pact is, and how it can help you test your code more efficiently, check out the Pact documentation.
Contains code originally from the pact-python project.
pactman is maintained by the ReeceTech team as part of their toolkit to keep their large (and growing) microservices architecture under control.
The key difference is all functionality is implemented in Python, rather than shelling out or forking to the ruby implementation. This allows for a much nicer mocking user experience (it mocks urllib3 directly), is faster, less messy configuration (multiple providers means multiple ruby processes spawned on different ports).
Where pact-python
required management of a background Ruby server, and manually starting and stopping
it, pactman
allows a much nicer usage like:
import requests
from pactman import Consumer, Provider
pact = Consumer('Consumer').has_pact_with(Provider('Provider'))
def test_interaction():
pact.given("some data exists").upon_receiving("a request") \
.with_request("get", "/", query={"foo": ["bar"]}).will_respond_with(200)
with pact:
requests.get(pact.uri, params={"foo": ["bar"]})
It also supports a broader set of the pact specification (versions 1.1 through to 3).
The pact verifier has been engineered from the start to talk to a pact broker (both to discover pacts and to return verification results).
There’s a few other quality of life improvements, but those are the big ones.
pactman
requires Python 3.6 to run.
pip install pactman
Creating a complete contract is a two step process:
If we have a method that communicates with one of our external services, which we'll call
Provider
, and our product, Consumer
is hitting an endpoint on Provider
at
/users/<user>
to get information about a particular user.
If the Consumer
's code to fetch a user looked like this:
import requests
def get_user(user_name):
response = requests.get(f'http://service.example/users/{user_name}')
return response.json()
Then Consumer
's contract test is a regular unit test, but using pactman for mocking,
and might look something like this:
import unittest
from pactman import Consumer, Provider
pact = Consumer('Consumer').has_pact_with(Provider('Provider'))
class GetUserInfoContract(unittest.TestCase):
def test_get_user(self):
expected = {
'username': 'UserA',
'id': 123,
'groups': ['Editors']
}
pact.given(
'UserA exists and is not an administrator'
).upon_receiving(
'a request for UserA'
).with_request(
'GET', '/users/UserA'
) .will_respond_with(200, body=expected)
with pact:
result = get_user('UserA')
self.assertEqual(result, expected)
This does a few important things:
given
to define the setup criteria for the Provider UserA exists and is not an administrator
Using the Pact object as a context manager, we call our method under test which will then communicate with the Pact mock. The mock will respond with the items we defined, allowing us to assert that the method processed the response and returned the expected value.
If you want more control over when the mock is configured and the interactions verified,
use the setup
and verify
methods, respectively:
Consumer('Consumer').has_pact_with(Provider('Provider')).given(
'UserA exists and is not an administrator'
).upon_receiving(
'a request for UserA'
).with_request(
'GET', '/users/UserA'
) .will_respond_with(200, body=expected)
pact.setup()
try:
# Some additional steps before running the code under test
result = get_user('UserA')
# Some additional steps before verifying all interactions have occurred
finally:
pact.verify()
You may have noticed that the pact relationship is defined at the module level in our examples:
pact = Consumer('Consumer').has_pact_with(Provider('Provider'))
This is because it must only be done once per test suite. By default the pact file is cleared out when that relationship is defined, so if you define it more than once per test suite you'll end up only storing the last pact declared per relationship. For more on this subject, see writing multiple pacts.
When defining the expected HTTP request that your code is expected to make you can specify the method, path, body, headers, and query:
pact.with_request(
method='GET',
path='/api/v1/my-resources/',
query={'search': 'example'}
)
query
is used to specify URL query parameters, so the above example expects
a request made to /api/v1/my-resources/?search=example
.
pact.with_request(
method='POST',
path='/api/v1/my-resources/123',
body={'user_ids': [1, 2, 3]},
headers={'Content-Type': 'application/json'},
)
You can define exact values for your expected request like the examples above, or you can use the matchers defined later to assist in handling values that are variable.
The has_pact_with(provider...)
call has quite a few options documented in its API, but a couple are
worth mentioning in particular:
version
declares the pact specification version that the provider supports. This defaults to "2.0.0", but "3.0.0"
is also acceptable if your provider supports Pact specification version 3:
from pactman import Consumer, Provider
pact = Consumer('Consumer').has_pact_with(Provider('Provider'), version='3.0.0')
file_write_mode
defaults to "overwrite"
and should be that or "merge"
. Overwrite ensures
that any existing pact file will be removed when has_pact_with()
is invoked. Merge will retain
the pact file and add new pacts to that file. See writing multiple pacts.
If you absolutely do not want pact files to be written, use "never"
.
use_mocking_server
defaults to False
and controls the mocking method used by pactman
. The default is to
patch urllib3
, which is the library underpinning requests
and is also used by some other projects. If you
are using a different library to make your HTTP requests which does not use urllib3
underneath then you will need
to set the use_mocking_server
argument to True
. This causes pactman
to run an actual HTTP server to mock the
requests (the server is listening on pact.uri
- use that to redirect your HTTP requests to the mock server.) You
may also set the PACT_USE_MOCKING_SERVER
environment variable to "yes" to force your entire suite to use the server
approach. You should declare the pact particpants (consumer and provider) outside of your tests and will need
to start and stop the mocking service outside of your tests too. The code below shows what using the server might
look like:
import atexit
from pactman import Consumer, Provider
pact = Consumer('Consumer').has_pact_with(Provider('Provider'), use_mocking_server=True)
pact.start_mocking()
atexit.register(pact.stop_mocking)
You'd then use pact
to declare pacts between those participants.
During a test run you're likely to need to write multiple pact interactions for a consumer/provider
relationship. pactman
will manage the pact file as follows:
has_pact_with()
is invoked it will by default remove any existing pact JSON file for the
stated consumer & provider.Consumer('Consumer').has_pact_with(Provider('Provider'))
once at the start of
your tests. This could be done as a pytest module or session fixture, or through some other
mechanism and store it in a variable. By convention this is called pact
in all of our examples.has_pact_with()
that it should either
retain (file_write_mode="merge"
) or remove (file_write_mode="overwrite"
) the existing
pact file.You use given()
to indicate to the provider that they should have some state in order to
be able to satisfy the interaction. You should agree upon the state and its specification
in discussion with the provider.
If you are defining a version 3 pact you may define provider states more richly, for example:
(pact
.given("this is a simple state as in v2")
.and_given("also the user must exist", username="alex")
)
Now you may specify additional parameters to accompany your provider state text. These are
passed as keyword arguments, and they're optional. You may also provider additional provider
states using the and_given()
call, which may be invoked many times if necessary. It and
given()
have the same calling convention: a provider state name and any optional parameters.
The default validity testing of equal values works great if that user information is always static, but what happens if the user has a last updated field that is set to the current time every time the object is modified? To handle variable data and make your tests more robust, there are several helpful matchers:
Available in version 3.0.0+ pacts
Asserts that the value should contain the given substring, for example::
from pactman import Includes, Like
Like({
'id': 123, # match integer, value varies
'content': Includes('spam', 'Sample spamming content') # content must contain the string "spam"
})
The matcher
and sample_data
are used differently by consumer and provider depending
upon whether they're used in the with_request()
or will_respond_with()
sections
of the pact. Using the above example:
When you run the tests for the consumer, the mock will verify that the data
the consumer uses in its request contains the matcher
string, raising an AssertionError
if invalid. When the contract is verified by the provider, the sample_data
will be
used in the request to the real provider service, in this case 'Sample spamming content'
.
When you run the tests for the consumer, the mock will return the data you provided
as sample_data
, in this case 'Sample spamming content'
. When the contract is verified on the
provider, the data returned from the real provider service will be verified to ensure it
contains the matcher
string.
Asserts the value should match the given regular expression. You could use this to expect a timestamp with a particular format in the request or response where you know you need a particular format, but are unconcerned about the exact date:
from pactman import Term
(pact
.given('UserA exists and is not an administrator')
.upon_receiving('a request for UserA')
.with_request(
'post',
'/users/UserA/info',
body={'commencement_date': Term('\d+-\d+-\d', '1972-01-01')})
.will_respond_with(200, body={
'username': 'UserA',
'last_modified': Term('\d+-\d+-\d+T\d+:\d+:\d+', '2016-12-15T20:16:01')
}))
The matcher
and sample_data
are used differently by consumer and provider depending
upon whether they're used in the with_request()
or will_respond_with()
sections
of the pact. Using the above example:
When you run the tests for the consumer, the mock will verify that the commencement_date
the consumer uses in its request matches the matcher
, raising an AssertionError
if invalid. When the contract is verified by the provider, the sample_data
will be
used in the request to the real provider service, in this case 1972-01-01
.
When you run the tests for the consumer, the mock will return the last_modified
you provided
as sample_data
, in this case 2016-12-15T20:16:01
. When the contract is verified on the
provider, the regex will be used to search the response from the real provider service
and the test will be considered successful if the regex finds a match in the response.
Asserts the element's type matches the sample_data
. For example:
from pactman import Like
Like(123) # Matches if the value is an integer
Like('hello world') # Matches if the value is a string
Like(3.14) # Matches if the value is a float
When you run the tests for the consumer, the mock will verify that values are
of the correct type, raising an AssertionError if invalid. When the contract is
verified by the provider, the sample_data
will be used in the request to the
real provider service.
When you run the tests for the consumer, the mock will return the sample_data
.
When the contract is verified on the provider, the values generated by the provider
service will be checked to match the type of sample_data
.
When a dictionary is used as an argument for Like, all the child objects (and their child objects etc.) will be matched according to their types, unless you use a more specific matcher like a Term.
from pactman import Like, Term
Like({
'username': Term('[a-zA-Z]+', 'username'),
'id': 123, # integer
'confirmed': False, # boolean
'address': { # dictionary
'street': '200 Bourke St' # string
}
})
Asserts the value is an array type that consists of elements
like sample_data
. It can be used to assert simple arrays:
from pactman import EachLike
EachLike(1) # All items are integers
EachLike('hello') # All items are strings
Or other matchers can be nested inside to assert more complex objects:
from pactman import EachLike, Term
EachLike({
'username': Term('[a-zA-Z]+', 'username'),
'id': 123,
'groups': EachLike('administrators')
})
Note, you do not need to specify everything that will be returned from the Provider in a JSON response, any extra data that is received will be ignored and the tests will still pass.
For more information see Matching
Available in version 3.0.0+ pacts
If you have a sub-term of a Like
which needs to match an exact value like the default
validity test then you can use Equals
, for example::
from pactman import Equals, Like
Like({
'id': 123, # match integer, value varies
'username': Equals('alex') # username must always be "alex"
})
The body
payload is assumed to be JSON data. In the absence of a Content-Type
header
we assume Content-Type: application/json; charset=UTF-8
(JSON text is Unicode and the
default encoding is UTF-8).
During verification non-JSON payloads are compared for equality.
During mocking, the HTTP response will be handled as:
Content-Type
header, assume JSON: serialise with json.dumps()
, encode to
UTF-8 and add the header Content-Type: application/json; charset=UTF-8
.Content-Type
header and it says application/json
then serialise with
json.dumps() and use the charset in the header, defaulting to UTF-8.Content-Type
header and body as-is.
Binary data is not supported.You have two options for verifying pacts against a service you created:
pactman-verifier
command-line program which replays the pact assertions against
a running instance of your service, orpytest
support built into pactman to replay the pacts as test cases, allowing
use of other testing mechanisms such as mocking and transaction control.pactman-verifier
Run pactman-verifier -h
to see the options available. To run all pacts registered to a provider in a Pact Broker:
pactman-verifier -b http://pact-broker.example/ <provider name> <provider url> <provider setup url>
You can pass in a local pact file with -l
, this will verify the service against the local file instead of the broker:
pactman-verifier -l /tmp/localpact.json <provider name> <provider url> <provider setup url>
You can use --custom-provider-header
to pass in headers to be passed to provider state setup and verify calls. it can
be used multiple times
pactman-verifier -b <broker url> --custom-provider-header "someheader:value" --custom-provider-header
"this:that" <provider name> <provider url> <provider state url>
An additional header may also be supplied in the PROVIDER_EXTRA_HEADER
environment variable, though the command
line argument(s) would override this.
In many cases, your contracts will need very specific data to exist on the provider to pass successfully. If you are fetching a user profile, that user needs to exist, if querying a list of records, one or more records needs to exist. To support decoupling the testing of the consumer and provider, Pact offers the idea of provider states to communicate from the consumer what data should exist on the provider.
When setting up the testing of a provider you will also need to setup the management of
these provider states. The Pact verifier does this by making additional HTTP requests to
the <provider setup url>
you provide. This URL could be
on the provider application or a separate one. Some strategies for managing state include:
For more information about provider states, refer to the Pact documentation on Provider States.
pytest
To verify pacts for a provider you would write a new pytest test module in the provider's test suite.
If you don't want it to be exercised in your usual unit test run you can call it verify_pacts.py
.
Your test code needs to use the pact_verifier
fixture provided by pactman, invoking
its verify()
method with the URL to the running instance of your service (pytest-django
provides
a handy live_server
fixture which works well here) and a callback to set up provider states (described
below).
You'll need to include some extra command-line arguments to pytest (also described below) to indicate where the pacts should come from, and whether verification results should be posted to a pact broker.
An example for a Django project might contain:
from django.contrib.auth.models import User
from pactman.verifier.verify import ProviderStateMissing
def provider_state(name, **params):
if name == 'the user "pat" exists':
User.objects.create(username='pat', fullname=params['fullname'])
else:
raise ProviderStateMissing(name)
def test_pacts(live_server, pact_verifier):
pact_verifier.verify(live_server.url, provider_state)
The pact_verifier.verify
call may also take a third argument to supply additional HTTP headers
to send to the server during verification - specify them as a dictionary.
The test function may do any level of mocking and data setup using standard pytest fixtures - so mocking downstream APIs or other interactions within the provider may be done with standard monkeypatching.
pytest
The provider_state
function passed to pact_verifier.verify
will be passed the providerState
and
providerStates
for all pacts being verified.
name
argument will be the providerState
value,
and params
will be empty.providerStates
array with the name
argument taken from the array entry name
parameter, and params
from
the params
parameter.pytest
verifying pactsOnce you have written the pytest code, you need to invoke pytest with additional arguments:
--pact-broker-url=<URL>
provides the base URL of the Pact broker to retrieve pacts from for the
provider. You must also provide --pact-provider-name=<ProviderName>
to identify which provider to
retrieve pacts for from the broker.
The broker URL and provider name may alternatively be provided through the environment variables
PACT_BROKER_URL
and PACT_PROVIDER_NAME
.
You may provide --pact-verify-consumer=<ConsumerName>
to limit
the pacts verified to just that consumer. As with the command-line verifier, you may provide basic
auth details in the broker URL, or through the PACT_BROKER_AUTH
environment variable. If your broker
requires a bearer token you may provide it with --pact-broker-token=<TOKEN>
or the PACT_BROKER_TOKEN
environment variable.
--pact-files=<file pattern>
verifies some on-disk pact JSON files identified by the wildcard pattern
(unix glob pattern matching, use **
to match multiple directories).
If you pulled the pacts from a broker and wish to publish verification results, use --pact-publish-results
to turn on publishing the results. This option also requires you to specify --pact-provider-version=<version>
.
So, for example:
# verify some local pacts in /tmp/pacts
$ pytest --pact-files=/tmp/pacts/*.json tests/verify_pacts.py
# verify some pacts in a broker for the provider MyService
$ pytest --pact-broker-url=http://pact-broker.example/ --pact-provider-name=MyService tests/verify_pacts.py
If you need to see the traceback that caused a pact failure you can use the verbosity flag
to pytest (pytest -v
).
See the "pact" section in the pytest command-line help (pytest -h
) for all command-line options.
You may also specify the broker URL in the environment variable PACT_BROKER_URL
.
If HTTP Basic Auth is required for the broker, that may be provided in the URL:
pactman-verifier -b http://user:password@pact-broker.example/ ...
pytest --pact-broker-url=http://user:password@pact-broker.example/ ...
or set in the PACT_BROKER_AUTH
environment variable as user:password
.
If your broker needs a bearer token then you may provide that on the command line or set it in the
environment variable PACT_BROKER_TOKEN
.
If your consumer pacts have tags (called "consumer version tags" because they attach to specific versions) then you may specify the tag(s) to fetch pacts for on the command line. Multiple tags may be specified, and all pacts matching any tags specified will be verified. For example, to ensure you're verifying your Provider against the production pact versions from your Consumers, use:
pactman-verifier --consumer-version-tag=production -b http://pact-broker.example/ ...
pytest --pact-verify-consumer-tag=production --pact-broker-url=http://pact-broker.example/ ...
Please read CONTRIBUTING.md
3.0.0 (FUTURE, DEPRECATION WARNINGS)
--pact-consumer-name
command-line option2.31.0
2.30.0
2.29.0
**
recursive globbing with --pact-files
, thanks @maksimt2.28.0
fail()
was not being invoked in an exact match
causing the pytest reporter to not know there'd been a failuresemver.parse
in semver2.27.0
--pact-verify-consumer-tag
from working2.26.0
extra_provider_headers
2.25.0
2.24.0
2.23.0
2.22.0
2.21.0
2.20.0
2.19.0
with interaction1, interaction2
instead of with pact
).2.18.0
2.17.0
2.16.0
2.15.0
"never"
to the file_write_mode
options.2.14.0
2.13.0
2.12.1
2.12.0
Equals
and Includes
matchers for pact v3+2.11.0
2.10.0
has_pact_with()
to accept file_write_mode
2.9.0
with_request
when called with a dict query (thanks Cong)start_mocking()
and stop_mocking()
optional with non-server mockingpython -m pactman.verifier.command_line
is just python -m pactman
(mostly used in testing before release)None
provider state2.8.0
2.7.0
and_given()
as a method of defining additonal provider states for v3+ pacts2.6.1
urlopen
didn't handle the correct number of positional arguments2.6.0
2.5.0
2.4.0
2.3.0
has_pact_with()
2.2.0
2.1.0
body
in the request2.0.0
1.2.0
1.1.0
pact-verifier
command to pactman-verifier
to avoid
confusion with other pre-existing packages that provide a command-line
incompatible pact-verifier
command.1.0.8
1.0.7
1.0.6
1.0.5
1.0.4