Package: pact-python
pact-pythonPython 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.
Currently supports version 2 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.Note: As of Version 1.0 deprecates support for python 2.7 to allow us to incorporate python 3.x features more readily. If you want to still use Python 2.7 use the 0.x.y versions. Only bug fixes will now be added to that release.How to use pact-pythonInstallationpip install pact-pythonGetting startedA guide follows but if you go to theexamples. This has a consumer, provider and pact-broker set of tests for both FastAPI and Flask.Writing a PactCreating a complete contract is a two step process:Create a 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 the code to fetch a user looked like this:importrequestsdefuser(user_name):"""Fetch a user object by user_name from the server."""uri='http://localhost:1234/users/'+user_namereturnrequests.get(uri).json()ThenConsumer's contract test might look something like this:importatexitimportunittestfrompactimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))pact.start_service()atexit.register(pact.stop_service)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=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 service. The mock service 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 service is
configured and the interactions verified, use thesetupandverifymethods, respectively:(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))pact.setup()# Some additional steps before running the code under testresult=user('UserA')# Some additional steps before verifying all interactions have occurredpact.verify()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.The default hostname and port for the Pact mock service will belocalhost:1234but you can adjust this during Pact creation:frompactimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'),host_name='mockservice',port=8080)This can be useful if you need to run to create more than one Pact for your test
because your code interacts with two different services. It is important to note
that the code you are testing with this contractmustcontact the mock service.
So in this example, theusermethod could accept an argument to specify the
location of the server, or retrieve it from an environment variable so you can
change its URI during the test.The mock service offers you several important features when building your contracts:It provides a real HTTP server that your code can contact during the test and provides the responses you defined.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.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.Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker.Expecting Variable ContentThe above test 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 3 helpful matchers:Term(matcher, generate)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:frompactimportTerm...body={'username':'UserA','last_modified':Term('\d+-\d+-\d+T\d+:\d+:\d+','2016-12-15T20:16:01')}(pact.given('UserA exists and is not an administrator').upon_receiving('a request for UserA').with_request('get','/users/UserA/info').will_respond_with(200,body=body))When you run the tests for the consumer, the mock service will return the value you provided
asgenerate, 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(matcher)Asserts the element's type matches the matcher. For example:frompactimportLikeLike(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 floatThe argument supplied toLikewill be what the mock service responds with.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.frompactimportLike,TermLike({'username':Term('[a-zA-Z]+','username'),'id':123,# integer'confirmed':False,# boolean'address':{# dictionary'street':'200 Bourke St'# string}})EachLike(matcher, minimum=1)Asserts the value is an array type that consists of elements
like the one passed in. It can be used to assert simple arrays:frompactimportEachLikeEachLike(1)# All items are integersEachLike('hello')# All items are stringsOr other matchers can be nested inside to assert more complex objects:frompactimportEachLike,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.Note, to get the generated values from an object that can contain matchers like Term, Like, EachLike, etc.
for assertion in self.assertEqual(result, expected) you may need to use get_generated_values() helper function:frompact.matchersimportget_generated_valuesself.assertEqual(result,get_generated_values(expected))Match common formatsOften times, you find yourself having to re-write regular expressions for common formats.frompactimportFormatFormat().integer# Matches if the value is an integerFormat().ip_address# Matches if the value is an ip addressWe've created a number of them for you to save you the time:matcherdescriptionidentifierMatch an ID (e.g. 42)integerMatch all numbers that are integers (both ints and longs)decimalMatch all real numbers (floating point and decimal)hexadecimalMatch all hexadecimal encoded stringsdateMatch string containing basic ISO8601 dates (e.g. 2016-01-01)timestampMatch a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400)timeMatch string containing times in ISO date format (e.g. T22:44:30.652Z)iso_datetimeMatch string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00)iso_datetime_msMatch string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00)ip_addressMatch string containing IP4 formatted addressipv6_addressMatch string containing IP6 formatted addressuuidMatch strings containing UUIDsThese can be used to replace other matchersfrompactimportLike,FormatLike({'id':Format().integer,# integer'lastUpdated':Format().timestamp,# timestamp'location':{# dictionary'host':Format().ip_address# ip address}})For more information seeMatchingUploading pact files to a Pact BrokerThere are two ways to publish your pact files, to a Pact Broker.Pact CLI toolsrecommendedPact Python APICLISeePublishing and retrieving pactsExample uploading to a Pact Brokerpact-broker publish /path/to/pacts/consumer-provider.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-username someUsername --broker-password somePasswordExample uploading to a PactFlow Brokerpact-broker publish /path/to/pacts/consumer-provider.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeTokenPython APIbroker=Broker(broker_base_url="http://localhost")broker.publish("TestConsumer","2.0.1",branch='consumer-branch',pact_dir='.')output,logs=verifier.verify_pacts('./userserviceclient-userservice.json')The parameters for this differ slightly in naming from their CLI equivalents:CLInative Python--branchbranch--build-urlbuild_url--auto-detect-version-propertiesauto_detect_version_properties--tag=TAGconsumer_tags--tag-with-git-branchtag_with_git_branchPACT_DIRS_OR_FILESpact_dir--consumer-app-versionversionn/aconsumer_nameVerifying Pacts Against a ServiceIn addition to writing Pacts for Python consumers, you can also verify those Pacts
against a provider of any language. There are two ways to do this.CLIAfter installing pact-python apact-verifierapplication should be available. To get details about its use you can call it with the
help argument:pact-verifier--helpThe simplest example is verifying a server with locally stored Pact files and no provider
states:pact-verifier--provider-base-url=http://localhost:8080--pact-url=./pacts/consumer-provider.jsonWhich will immediately invoke the Pact verifier, making HTTP requests to the server located
athttp://localhost:8080based on the Pacts in./pacts/consumer-provider.jsonand
reporting the results.There are several options for configuring how the Pacts are verified:--provider-base-urlRequired. Defines the URL of the server to make requests to when verifying the Pacts.--pact-urlRequired if --pact-urls not specified. The location of a Pact file you want
to verify. This can be a URL to aPact Brokeror a local path, to provide
multiple files, specify multiple arguments.pact-verifier --provider-base-url=http://localhost:8080 --pact-url=./pacts/one.json --pact-url=./pacts/two.json--pact-urlsRequired if --pact-url not specified. The location of the Pact files you want
to verify. This can be a URL to aPact Brokeror one or more local paths, separated by a comma.--provider-states-urlDEPRECATED AFTER v 0.6.0.The URL where your provider application will produce the list of available provider states.
The verifier calls this URL to ensure the Pacts specify valid states before making the HTTP
requests.--provider-states-setup-urlThe URL which should be called to setup a specific provider state before a Pact is verified. This URL will be called with a POST request, and the JSON body{consumer: 'Consumer name', state: 'a thing exists'}.--pact-broker-urlBase URl for the Pact Broker instance to publish pacts to. Can also be specified via the environment variablePACT_BROKER_BASE_URL.--pact-broker-usernameThe username to use when contacting the Pact Broker. Can also be specified via the environment variablePACT_BROKER_USERNAME.--pact-broker-passwordThe password to use when contacting the Pact Broker. You can also specify this value
as the environment variablePACT_BROKER_PASSWORD.--pact-broker-tokenThe bearer token to use when contacting the Pact Broker. You can also specify this value
as the environment variablePACT_BROKER_TOKEN.--consumer-version-tagRetrieve the latest pacts with this consumer version tag. Used in conjunction with--provider.
May be specified multiple times.--consumer-version-selectorYou can also retrieve pacts with consumer version selector, a more flexible approach in specifying which pacts you need.
May be specified multiple times. Read more about selectorshere.--provider-version-tagTag to apply to the provider application version. May be specified multiple times.--provider-version-branchBranch to apply to the provider application version.--custom-provider-headerHeader to add to provider state set up and pact verification requests e.g.Authorization: Basic cGFjdDpwYWN0May be specified multiple times.-t, --timeoutThe duration in seconds we should wait to confirm that the verification process was successful. Defaults to 30.-a, --provider-app-versionThe provider application version. Required for publishing verification results.-r, --publish-verification-resultsPublish verification results to the broker.Python APIYou can use the Verifier class. This allows you to write native python code and the test framework of your choice.verifier=Verifier(provider='UserService',provider_base_url=PACT_URL)# Using a local pact filesuccess,logs=verifier.verify_pacts('./userserviceclient-userservice.json')assertsuccess==0# Using a pact broker-ForOSSPactBroker,usebroker_username/broker_password-ForPactFlowPactBroker,usebroker_tokensuccess,logs=verifier.verify_with_broker(# broker_username=PACT_BROKER_USERNAME,# broker_password=PACT_BROKER_PASSWORD,broker_url=PACT_BROKER_URL,broker_token=PACT_BROKER_TOKEN,publish_version=APPLICATION_VERSION,publish_verification_results=True,verbose=True,provider_version_branch=PROVIDER_BRANCH,enable_pending=True,)assertsuccess==0The parameters for this differ slightly in naming from their CLI equivalents:CLInative Python--log-dirlog_dir--log-levellog_level--provider-app-versionprovider_app_version--headerscustom_provider_headers--consumer-version-tagconsumer_tags--provider-version-tagprovider_tags--provider-states-setup-urlprovider_states_setup_url--verboseverbose--consumer-version-selectorconsumer_selectors--publish-verification-resultspublish_verification_results--provider-version-branchprovider_version_branchYou can see more details in the examplesMessage Provider Verifier TestFlask Provider Verifier TestFastAPI Provider Verifier TestProvider 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-states-setup-urlyou 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.DevelopmentPlease readCONTRIBUTING.mdTo setup a development environment:If you want to run tests for all Python versions, install 2.7, 3.3, 3.4, 3.5, and 3.6 from source or using a tool likepyenvIts recommended to create a Pythonvirtualenvfor the projectTo setup the environment, run tests, and package the application, run:make releaseIf you are just interested in packaging pact-python so you can install it using pip:make packageThis creates adist/pact-python-N.N.N.tar.gzfile, where the Ns are the current version.
From there you can use pip to install it:pip install ./dist/pact-python-N.N.N.tar.gzOffline Installation of Standalone PackagesAlthough all Ruby standalone applications are predownloaded into the wheel artifact, it may be useful, for development, purposes to install custom Ruby binaries. In which case, use thebin-pathflag.pip install pact-python --bin-path=/absolute/path/to/folder/containing/pact/binaries/for/your/osPact binaries can be found atPact Ruby Releases.TestingThis project has unit and end to end tests, which can both be run from make:Unit:make testEnd to end:make e2eContactJoin us in slack:orTwitter:@pact_upStack Overflow:stackoverflow.com/questions/tagged/pact