Package: pactman
pactmanPython version of Pact mocking, generation and verification.Enablesconsumer 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 thePact specification.For more information about what Pact is, and how it can help you
test your code more efficiently, check out thePact documentation.Contains code originally from thepact-pythonproject.pactman is maintained by theReeceTechteam as part of their toolkit to
keep their large (and growing) microservices architecture under control.How to use pactmanInstallationWriting a PactWriting the Consumer TestExpecting Variable ContentVerifying Pacts Against a ServiceDevelopmentRelease Historypactman vs pact-pythonThe 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).Wherepact-pythonrequired management of a background Ruby server, and manually starting and stopping
it,pactmanallows a much nicer usage like:importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request")\.with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact: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.How to use pactmanInstallationpactmanrequires Python 3.6 to run.pip install pactmanWriting a PactCreating a complete contract is a two step process:Create a unit test on the consumer side that declares the expectations it has of the providerCreate a provider state that allows the contract to pass when replayed against the providerWriting the Consumer TestIf we have a method that communicates with one of our external services, which we'll callProvider, and our product,Consumeris hitting an endpoint onProviderat/users/<user>to get information about a particular user.If theConsumer's code to fetch a user looked like this:importrequestsdefget_user(user_name):response=requests.get(f'http://service.example/users/{user_name}')returnresponse.json()ThenConsumer's contract test is a regular unit test, butusing pactman for mocking,
and might look something like this:importunittestfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))classGetUserInfoContract(unittest.TestCase):deftest_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)withpact:result=get_user('UserA')self.assertEqual(result,expected)This does a few important things:Defines the Consumer and Provider objects that describe our product and our service under testUsesgivento define the setup criteria for the ProviderUserA exists and is not an administratorDefines what the request that is expected to be made by the consumer will containDefines how the server is expected to respondUsing the Pact object as acontext 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 thesetupandverifymethods, 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 testresult=get_user('UserA')# Some additional steps before verifying all interactions have occurredfinally:pact.verify()An important note about pact relationship definitionYou 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 itmust only be done onceper 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 thelastpact declared per relationship. For more on this
subject, seewriting multiple pacts.RequestsWhen 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'})queryis 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.Some important has_pact_with() options()Thehas_pact_with(provider...)call has quite a few options documented in its API, but a couple are
worth mentioning in particular:versiondeclares the pact specification version that the provider supports. This defaults to "2.0.0", but "3.0.0"
is also acceptable if your provider supportsPact specification version 3:frompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'),version='3.0.0')file_write_modedefaults to"overwrite"and should be that or"merge". Overwrite ensures
that any existing pact file will be removed whenhas_pact_with()is invoked. Merge will retain
the pact file and add new pacts to that file. Seewriting multiple pacts.
If you absolutely do not want pact files to be written, use"never".use_mocking_serverdefaults toFalseand controls the mocking method used bypactman. The default is to
patchurllib3, which is the library underpinningrequestsand is also used by some other projects. If you
are using a different library to make your HTTP requests which does not useurllib3underneath then you will need
to set theuse_mocking_serverargument toTrue. This causespactmanto run an actual HTTP server to mock the
requests (the server is listening onpact.uri- use that to redirect your HTTP requests to the mock server.) You
may also set thePACT_USE_MOCKING_SERVERenvironment 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:importatexitfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'),use_mocking_server=True)pact.start_mocking()atexit.register(pact.stop_mocking)You'd then usepactto declare pacts between those participants.Writing multiple pactsDuring a test run you're likely to need to write multiple pact interactions for a consumer/provider
relationship.pactmanwill manage the pact file as follows:Whenhas_pact_with()is invoked it will by default remove any existing pact JSON file for the
stated consumer & provider.You may invokeConsumer('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 calledpactin all of our examples.If that is not suitable, you may manually indicate tohas_pact_with()that it should either
retain (file_write_mode="merge") or remove (file_write_mode="overwrite") the existing
pact file.Some words about given()You usegiven()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 theand_given()call, which may be invoked many times if necessary. It andgiven()have the same calling convention: a provider state name and any optional parameters.Expecting Variable ContentThe 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:Includes(matcher, sample_data)Available in version 3.0.0+ pactsAsserts that the value should contain the given substring, for example::frompactmanimportIncludes,LikeLike({'id':123,# match integer, value varies'content':Includes('spam','Sample spamming content')# content must contain the string "spam"})Thematcherandsample_dataare used differently by consumer and provider depending
upon whether they're used in thewith_request()orwill_respond_with()sections
of the pact. Using the above example:Includes in requestWhen you run the tests for the consumer, the mock will verify that the data
the consumer uses in its request contains thematcherstring, raising an AssertionError
if invalid. When the contract is verified by the provider, thesample_datawill be
used in the request to the real provider service, in this case'Sample spamming content'.Includes in responseWhen you run the tests for the consumer, the mock will return the data you provided
assample_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 thematcherstring.Term(matcher, sample_data)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:frompactmanimportTerm(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')}))Thematcherandsample_dataare used differently by consumer and provider depending
upon whether they're used in thewith_request()orwill_respond_with()sections
of the pact. Using the above example:Term in requestWhen you run the tests for the consumer, the mock will verify that thecommencement_datethe consumer uses in its request matches thematcher, raising an AssertionError
if invalid. When the contract is verified by the provider, thesample_datawill be
used in the request to the real provider service, in this case1972-01-01.Term in responseWhen you run the tests for the consumer, the mock will return thelast_modifiedyou provided
assample_data, in this case2016-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.Like(sample_data)Asserts the element's type matches thesample_data. For example:frompactmanimportLikeLike(123)# Matches if the value is an integerLike('hello world')# Matches if the value is a stringLike(3.14)# Matches if the value is a floatLike in requestWhen 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, thesample_datawill be used in the request to the
real provider service.Like in responseWhen you run the tests for the consumer, the mock will return thesample_data.
When the contract is verified on the provider, the values generated by the provider
service will be checked to match the type ofsample_data.Applying Like to complex data structuresWhen 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.frompactmanimportLike,TermLike({'username':Term('[a-zA-Z]+','username'),'id':123,# integer'confirmed':False,# boolean'address':{# dictionary'street':'200 Bourke St'# string}})EachLike(sample_data, minimum=1)Asserts the value is an array type that consists of elements
likesample_data. It can be used to assert simple arrays:frompactmanimportEachLikeEachLike(1)# All items are integersEachLike('hello')# All items are stringsOr other matchers can be nested inside to assert more complex objects:frompactmanimportEachLike,TermEachLike({'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 seeMatchingEnforcing equality matching with EqualsAvailable in version 3.0.0+ pactsIf you have a sub-term of aLikewhich needs to match an exact value like the default
validity test then you can useEquals, for example::frompactmanimportEquals,LikeLike({'id':123,# match integer, value varies'username':Equals('alex')# username must always be "alex"})Body payload rulesThebodypayload is assumed to be JSON data. In the absence of aContent-Typeheader
we assumeContent-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:If there's noContent-Typeheader, assume JSON: serialise withjson.dumps(), encode to
UTF-8 and add the headerContent-Type: application/json; charset=UTF-8.If there's aContent-Typeheader and it saysapplication/jsonthen serialise with
json.dumps() and use the charset in the header, defaulting to UTF-8.Otherwise pass through theContent-Typeheader and body as-is.
Binary data is not supported.Verifying Pacts Against a ServiceYou have two options for verifying pacts against a service you created:Use thepactman-verifiercommand-line program which replays the pact assertions against
a running instance of your service, orUse thepytestsupport built into pactman to replay the pacts as test cases, allowing
use of other testing mechanisms such as mocking and transaction control.Usingpactman-verifierRunpactman-verifier -hto see the options available. To run all pacts registered to a provider in aPact 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-headerto pass in headers to be passed to provider state setup and verify calls. it can
be used multiple timespactman-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 thePROVIDER_EXTRA_HEADERenvironment variable, though the command
line argument(s) would override this.Provider StatesIn 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:Having endpoints in your application that are not active in production that create and delete your datastore stateA 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 datastoreA standalone application that can start and stop the other server with different datastore statesFor more information about provider states, refer to thePact documentationonProvider States.Verifying Pacts UsingpytestTo 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 itverify_pacts.py.Your test code needs to use thepact_verifierfixture provided by pactman, invoking
itsverify()method with the URL to the running instance of your service (pytest-djangoprovides
a handylive_serverfixture 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:fromdjango.contrib.auth.modelsimportUserfrompactman.verifier.verifyimportProviderStateMissingdefprovider_state(name,**params):ifname=='the user "pat" exists':User.objects.create(username='pat',fullname=params['fullname'])else:raiseProviderStateMissing(name)deftest_pacts(live_server,pact_verifier):pact_verifier.verify(live_server.url,provider_state)Thepact_verifier.verifycall 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.Provider states usingpytestTheprovider_statefunction passed topact_verifier.verifywill be passed theproviderStateandproviderStatesfor all pacts being verified.For pacts withproviderStatethenameargument will be theproviderStatevalue,
andparamswill be empty.For pacts withproviderStatesthe function will be invoked once per entry inproviderStatesarray with thenameargument taken from the array entrynameparameter, andparamsfrom
theparamsparameter.Command line options to controlpytestverifying 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 variablesPACT_BROKER_URLandPACT_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 thePACT_BROKER_AUTHenvironment variable. If your broker
requires a bearer token you may provide it with--pact-broker-token=<TOKEN>or thePACT_BROKER_TOKENenvironment 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-resultsto 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/*.jsontests/verify_pacts.py# verify some pacts in a broker for the provider MyService$pytest--pact-broker-url=http://pact-broker.example/--pact-provider-name=MyServicetests/verify_pacts.pyIf 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.Pact Broker ConfigurationYou may also specify the broker URL in the environment variablePACT_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 thePACT_BROKER_AUTHenvironment variable asuser:password.If your broker needs a bearer token then you may provide that on the command line or set it in the
environment variablePACT_BROKER_TOKEN.Filtering Broker Pacts by TagIf 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 theproductionpact 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/ ...DevelopmentPlease readCONTRIBUTING.mdRelease History3.0.0 (FUTURE, DEPRECATION WARNINGS)remove DEPRECATED--pact-consumer-namecommand-line option2.30.0DELETE requests may now have query strings, thanks @MazeDeveloperNicer feedback if no pact source is specified on command line, thanks @artamonovkirillAdd PACT_PROVIDER_NAME to environment vars, thanks @artamonovkirill2.29.0Added support for**recursive globbing with--pact-files, thanks @maksimt2.28.0Fixed edge case wherefail()was not being invoked in an exact match
causing the pytest reporter to not know there'd been a failureAddress deprecation ofsemver.parsein semverDropped Python 3.6 testing, added Python 3.8 testing2.27.0Fix typo in pytest plugin preventing--pact-verify-consumer-tagfrom workingAdded PATCH support2.26.0Allow pytest verification to specifyextra_provider_headers2.25.0Add option to allow pytest to succeed even if a pact verification fails2.24.0Better integration of pact failure information in pytest2.23.0Enable setting of authentication credentials when connecting to the pact brokerAllow filtering of pacts fetched from broker to be filtered by consumer version tagImprove the naming and organisation of the pytest command line options2.22.0Better implementation of change in 2.21.02.21.0Handle warning level messages in command line output handler2.20.0Fix pytest mode to correctly detect array element rule failure as a pytest failureAllow restricting pytest verification runs to a single consumer using --pact-consumer-name2.19.0Correct teardown of pact context manager where the pact is used in multiple
interactions (with interaction1, interaction2instead ofwith pact).2.18.0Correct bug in cleanup that resulted in urllib mocking breaking.2.17.0Handle absence of any provider state (!) in pytest setup.2.16.0Delay shenanigans around checking pacts directory until pacts are actually written
to allow module-level pact definition without side effects.2.15.0Fix structure of serialisation for header matching rules.Add"never"to thefile_write_modeoptions.Handle x-www-form-urlencoded POST request bodies.2.14.0Improve verbose messages to clarify what they're saying.2.13.0Add ability to supply additional headers to provider during verification (thanks @ryallsa)2.12.1Fix pact-python Term compatibility2.12.0AddEqualsandIncludesmatchers for pact v3+Make verification fail if missing header specified in interactionSignificantly improved support for pytest provider verification of pactsTurned pact state call failures into warnings rather than errors2.11.0Ensure query param values are lists2.10.0Allowhas_pact_with()to acceptfile_write_modeFix bug introduced in 2.9.0 where generating multiple pacts would result in a single pact
being recorded2.9.0Fixwith_requestwhen called with a dict query (thanks Cong)Makestart_mocking()andstop_mocking()optional with non-server mockingAdd shortcut sopython -m pactman.verifier.command_lineis justpython -m pactman(mostly used in testing before release)Handle theNoneprovider stateEnsure pact spec versions are consistent across all mocks used to generate a pact file2.8.0Close up some edge cases in body content during mocking, and document in README2.7.0Addedand_given()as a method of defining additonal provider states for v3+ pactsAdded more tests for pact generation (serialisation) which fixed a few edge case bugsFix handling of lower-case HTTP methods in verifier (thanks Cong!)2.6.1Fix issue where mockedurlopendidn't handle the correct number of positional arguments2.6.0Fix several issues cause by a failure to detect failure in several test cases
(header, path and array element rules may not have been applied)Fix rules applying to a single non-first element in an arrayFix generation of consumer / provider name in <v3 pacts2.5.0Fix some bugs around empty array verification2.4.0Create the pact destination dir if it's missing and its parent exists2.3.0Fix some issues around mocking request queries and the mock's verification of sameFix header regex matching in mock verificationActually use the version passed in tohas_pact_with()Fix some pact v3 generation issues (thanks pan Jacek)2.2.0Reinstate lost result output.2.1.0Corrected the definition of request payload when there is nobodyin the request2.0.0Correctly determine pact verification result when publishing to broker.1.2.0Corrected use of format_path in command line error handling.Tweaked README for clarity.1.1.0Renamed thepact-verifiercommand topactman-verifierto avoid
confusion with other pre-existing packages that provide a command-line
incompatiblepact-verifiercommand.Support verification of HEAD requests (oops).1.0.8Corrected project URL in project metadata (thanks Jonathan Moss)Fix verbose output1.0.7Added some Trove classifiers to aid potential users.1.0.6Corrected mis-named command-line option.1.0.5Corrected some packaging issues1.0.4Initial release of pactman, including ReeceTech's pact-verifier version 3.17 and pact-python version 0.17.0