|
1 | 1 | # pact-python |
2 | | -Python 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. |
| 2 | +Python version of Pact. Enables consumer driven contract testing, |
| 3 | +providing a mock service and DSL for the consumer project, and |
| 4 | +interaction playback and verification for the service provider project. |
| 5 | + |
| 6 | +For more information about what Pact is, and how it can help you |
| 7 | +test your code more efficiently, check out the [Pact documentation]. |
| 8 | + |
| 9 | +# How to use pact-python |
| 10 | + |
| 11 | +## Installation |
| 12 | +``` |
| 13 | +pip install pact-python |
| 14 | +``` |
| 15 | + |
| 16 | +## Writing a Pact |
| 17 | +Creating a complete contract is a two step process: |
| 18 | + |
| 19 | +1. Create a test on the consumer side that declares the expectations it has of the provider |
| 20 | +2. Create a provider state that allows the contract to pass when replayed against the provider |
| 21 | + |
| 22 | +## Writing the Consumer Test |
| 23 | + |
| 24 | +If we have a method that communicates with one of our external services, which we'll call |
| 25 | +`Provider`, and our product, `Consumer` is hitting an endpoint on `Provider` at |
| 26 | +`/users/<user>` to get information about a particular user. |
| 27 | + |
| 28 | +If the code to fetch a user looked like this: |
| 29 | + |
| 30 | +```python |
| 31 | +import requests |
| 32 | + |
| 33 | + |
| 34 | +def user(user_name): |
| 35 | + """Fetch a user object by user_name from the server.""" |
| 36 | + uri = 'http://localhost:1234/users/' + user_name |
| 37 | + return requests.get(uri).json() |
| 38 | +``` |
| 39 | + |
| 40 | +Then `Consumer`'s contract test might look something like this: |
| 41 | + |
| 42 | +```python |
| 43 | +import unittest |
| 44 | + |
| 45 | +from pact import Consumer, Provider |
| 46 | + |
| 47 | + |
| 48 | +class GetUserInfoContract(unittest.TestCase): |
| 49 | + def test_get_user(self): |
| 50 | + pact = Consumer('Consumer').has_pact_with(Provider('Provider')) |
| 51 | + expected = { |
| 52 | + 'username': 'UserA', |
| 53 | + 'id': 123, |
| 54 | + 'groups': ['Editors'] |
| 55 | + } |
| 56 | + |
| 57 | + (pact |
| 58 | + .given('UserA exists and is not an administrator') |
| 59 | + .upon_receiving('a request for UserA') |
| 60 | + .with_request('get', '/users/UserA') |
| 61 | + .will_respond_with(200, body=expected)) |
| 62 | + |
| 63 | + with pact: |
| 64 | + result = user('UserA') |
| 65 | + |
| 66 | + self.assertEqual(result, expected) |
| 67 | + |
| 68 | +``` |
| 69 | + |
| 70 | +This does a few important things: |
| 71 | + |
| 72 | + - Defines the Consumer and Provider objects that describe our product and our service under test |
| 73 | + - Uses `given` to define the setup criteria for the Provider `UserA exists and is not an administrator` |
| 74 | + - Defines what the request that is expected to be made by the consumer will contain |
| 75 | + - Defines how the server is expected to respond |
| 76 | + |
| 77 | +Using the Pact object as a [context manager], we call our method under test |
| 78 | +which will then communicate with the Pact mock service. The mock service will respond with |
| 79 | +the items we defined, allowing us to assert that the method processed the response and |
| 80 | +returned the expected value. |
| 81 | + |
| 82 | +The default hostname and port for the Pact mock service will be |
| 83 | +`localhost:1234` but you can adjust this during Pact creation: |
| 84 | + |
| 85 | +```python |
| 86 | +from pact import Consumer, Provider |
| 87 | +pact = Consumer('Consumer').has_pact_with( |
| 88 | + Provider('Provider'), host_name='mockservice', port=8080) |
| 89 | +``` |
| 90 | + |
| 91 | +This can be useful if you are running your tests and the mock service inside a Docker |
| 92 | +network, where you want to reference the service by its Docker name, instead of via |
| 93 | +the `localhost` interface. It is important to note that the code you are testing with |
| 94 | +this contract _must_ contact the mock service. So in this example, the `user` method |
| 95 | +could accept an argument to specify the location of the server, or retrieve it from an |
| 96 | +environment variable so you can change its URI during the test. Another option is to |
| 97 | +specify a Docker network alias so the requests that you make will go to the container. |
| 98 | + |
| 99 | +The mock service offers you several important features when building your contracts: |
| 100 | +- It provides a real HTTP server that your code can contact during the test and provides the responses you defined. |
| 101 | +- 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. |
| 102 | +- 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. |
| 103 | +- Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker. |
| 104 | + |
| 105 | +## Expecting Variable Content |
| 106 | +The above test works great if that user information is always static, but what happens if |
| 107 | +the user has a last updated field that is set to the current time every time the object is |
| 108 | +modified? To handle variable data and make your tests more robust, there are 3 helpful matchers: |
| 109 | + |
| 110 | +### Term(matcher, generate) |
| 111 | +Asserts the value should match the given regular expression. You could use this |
| 112 | +to expect a timestamp with a particular format in the request or response where |
| 113 | +you know you need a particular format, but are unconcerned about the exact date: |
| 114 | + |
| 115 | +```python |
| 116 | +from pact import Term |
| 117 | +... |
| 118 | +body = { |
| 119 | + 'username': 'UserA', |
| 120 | + 'last_modified': Term('\d+-\d+-\d+T\d+:\d+:\d+', '2016-12-15T20:16:01') |
| 121 | +} |
| 122 | + |
| 123 | +(pact |
| 124 | + .given('UserA exists and is not an administrator') |
| 125 | + .upon_receiving('a request for UserA') |
| 126 | + .with_request('get', '/users/UserA/info') |
| 127 | + .will_respond_with(200, body=body)) |
| 128 | +``` |
| 129 | + |
| 130 | +When you run the tests for the consumer, the mock service will return the value you provided |
| 131 | +as `generate`, in this case `2016-12-15T20:16:01`. When the contract is verified on the |
| 132 | +provider, the regex will be used to search the response from the real provider service |
| 133 | +and the test will be considered successful if the regex finds a match in the response. |
| 134 | + |
| 135 | +### SomethingLike(matcher) |
| 136 | +Asserts the element's type matches the matcher. For example: |
| 137 | + |
| 138 | +```python |
| 139 | +from pact import SomethingLike |
| 140 | +SomethingLike(123) # Matches if the value is an integer |
| 141 | +SomethingLike('hello world') # Matches if the value is a string |
| 142 | +SomethingLike(3.14) # Matches if the value is a float |
| 143 | +``` |
| 144 | + |
| 145 | +The argument supplied to `SomethingLike` will be what the mock service responds with. |
| 146 | + |
| 147 | +### EachLike(matcher, minimum=None, maximum=None) |
| 148 | +Asserts the value is an array type that consists of elements |
| 149 | +like the ones passed in. It can be used to assert simple arrays: |
| 150 | + |
| 151 | +```python |
| 152 | +from pact import EachLike |
| 153 | +EachLike(1) # All items are integers |
| 154 | +EachLike('hello') # All items are strings |
| 155 | +``` |
| 156 | + |
| 157 | +Or other matchers can be nested inside to assert more complex objects: |
| 158 | + |
| 159 | +```python |
| 160 | +from pact import EachLike, SomethingLike, Term |
| 161 | +EachLike({ |
| 162 | + 'username': Term('[a-zA-Z]+', 'username'), |
| 163 | + 'id': SomethingLike(123), |
| 164 | + 'groups': EachLike('administrators') |
| 165 | +}) |
| 166 | +``` |
| 167 | + |
| 168 | +> Note, you do not need to specify everything that will be returned from the Provider in a |
| 169 | +> JSON response, any extra data that is received will be ignored and the tests will still pass. |
| 170 | +
|
| 171 | +For more information see [Matching](https://docs.pact.io/documentation/matching.html) |
| 172 | + |
| 173 | +## Running the Mock Service |
| 174 | +This library does not yet automatically handle running the [Pact Mock Service] so you will need to |
| 175 | +start that manually before running the tests. There are two primary ways to run the mock service: |
| 176 | + |
| 177 | +1. [Install it and run it using Ruby](https://github.com/bethesque/pact-mock_service#usage) |
| 178 | +2. Run it as a Docker container |
| 179 | + |
| 180 | +Using the Docker container additionally has two options. You can run it via the `docker` command: |
| 181 | + |
| 182 | +``` |
| 183 | +docker run -d -p "1234:1234" -v /tmp/log:/var/log/pacto -v $(pwd)/contracts:/opt/contracts madkom/pact-mock-service |
| 184 | +``` |
| 185 | + |
| 186 | +Which will start the service and expose it as port `1234` on your computer, mount |
| 187 | +`/tmp/log` on your machine to house the mock service log files, and the directory |
| 188 | +`contracts` in the current working directory to house the contracts when they are published. |
| 189 | + |
| 190 | +Additionally, you could run the mock service using `docker-compose`: |
| 191 | + |
| 192 | +```yaml |
| 193 | +version: '2' |
| 194 | +services: |
| 195 | + pactmockservice: |
| 196 | + image: madkom/pact-mock-service |
| 197 | + ports: |
| 198 | + - "1234:1234" |
| 199 | + volumes: |
| 200 | + - /tmp/pact:/var/log/pacto |
| 201 | + - ./contracts:/opt/contracts |
| 202 | +``` |
| 203 | +
|
| 204 | +> Note: How you run the mock service may change what hostname and port you should |
| 205 | +> use when running your consumer tests. For example: If you change the host port on |
| 206 | +> the command line to be `8080`, your tests would need to contact `localhost:8080`. |
| 207 | + |
| 208 | +## Verifying Pacts Against a Service |
| 209 | +> pact-python does not yet have any involvement in the process of verifying a contract against |
| 210 | +> a provider. This section is included to provide insight into the full cycle of a |
| 211 | +> contract for those getting started. |
| 212 | + |
| 213 | +Like the mock service, the provider verifier can be run in two ways: |
| 214 | + |
| 215 | +1. [Install and use it as a Ruby application][pact-provider-verifier] |
| 216 | +2. Run it as a Docker container |
| 217 | + |
| 218 | +> Both choices have very similar configuration options. We will illustrate the Docker |
| 219 | +> method below, but the Ruby method supports the same features. |
| 220 | + |
| 221 | +When verifying your contracts, you may find it easier to run the provider application |
| 222 | +and the verifier in separate Docker containers. This gives you a nice isolated |
| 223 | +network, where you can set the DNS records of the services to anything you desire |
| 224 | +and not have to worry about port conflicts with other services on your computer. |
| 225 | +Launching the provider verifier in a `docker-compose.yml` might look like this: |
| 226 | + |
| 227 | +```yaml |
| 228 | +version: '2' |
| 229 | +services: |
| 230 | + app: |
| 231 | + image: the-provider-application-to-test |
| 232 | +
|
| 233 | + pactverifier: |
| 234 | + command: ['tail', '-f', '/dev/null'] |
| 235 | + image: dius/pact-provider-verifier-docker |
| 236 | + depends_on: |
| 237 | + - app |
| 238 | + volumes: |
| 239 | + - ./contracts:/tmp/pacts |
| 240 | + environment: |
| 241 | + - pact_urls=/tmp/pacts/consumer-provider.json |
| 242 | + - provider_base_url=http://app |
| 243 | + - provider_states_url=http://app/_pact/provider-states |
| 244 | + - provider_states_active_url=http://app/_pact/provider-states/active |
| 245 | +``` |
| 246 | + |
| 247 | +In this example, our `app` container may take a few moments to start, so we don't |
| 248 | +immediately start running the verification, and instead `tail -f /dev/null` which will keep |
| 249 | +the container running forever. We can then use `docker-compose` to run the tests like so: |
| 250 | + |
| 251 | +``` |
| 252 | +docker-compose up -d |
| 253 | +# Insert code to check that `app` has finished starting and is ready for requests |
| 254 | +docker-compose exec pactverifier bundle exec rake verify_pacts |
| 255 | +``` |
| 256 | + |
| 257 | +You configure the verifier in Docker using 4 environment variables: |
| 258 | +- `pact_urls` - a comma delimited list of pact file urls |
| 259 | +- `provider_base_url` - the base url of the pact provider |
| 260 | +- `provider_states_url` - the full url of the endpoint which returns provider states by consumer |
| 261 | +- `provider_states_active_url` - the full url of the endpoint which sets the active pact consumer and provider state |
| 262 | + |
| 263 | +### Provider States |
| 264 | +In many cases, your contracts will need very specific data to exist on the provider |
| 265 | +to pass successfully. If you are fetching a user profile, that user needs to exist, |
| 266 | +if querying a list of records, one or more records needs to exist. To support |
| 267 | +decoupling the testing of the consumer and provider, Pact offers the idea of provider |
| 268 | +states to communicate from the consumer what data should exist on the provider. |
| 269 | + |
| 270 | +When setting up the testing of a provider you will also need to setup the management of |
| 271 | +these provider states. The Pact verifier does this by making additional HTTP requests to |
| 272 | +the `provider_states_url` and `provider_stats_active_url` you provide. These URLs could be |
| 273 | +on the provider application or a separate one. Some strategies for managing state include: |
| 274 | + |
| 275 | +- Having endpoints in your application that are not active in production that create and delete your datastore state |
| 276 | +- A 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 datastore |
| 277 | +- A standalone application that can start and stop the other server with different datastore states |
| 278 | + |
| 279 | +For more information about provider states, refer to the [Pact documentation] on [Provider States]. |
| 280 | + |
| 281 | +# Development |
| 282 | +Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) |
| 283 | + |
| 284 | +Create a Python virtualenv for use with this project |
| 285 | +`make release` |
| 286 | + |
| 287 | +## Testing |
| 288 | +Unit: `make test` |
| 289 | + |
| 290 | +End to end: `make e2e` |
| 291 | + |
| 292 | +[context manager]: https://en.wikibooks.org/wiki/Python_Programming/Context_Managers |
| 293 | +[Pact]: https://www.gitbook.com/book/pact-foundation/pact/details |
| 294 | +[Pact documentation]: https://docs.pact.io/ |
| 295 | +[Pact Mock Service]: https://github.com/bethesque/pact-mock_service |
| 296 | +[Provider States]: https://docs.pact.io/documentation/provider_states.html |
| 297 | +[pact-provider-verifier]: https://github.com/pact-foundation/pact-provider-verifier |
0 commit comments