Skip to content

patricekrakow/play-with-pact

Repository files navigation

Let's Play with Pact

Abstract

This is an attempt to create the simplest demo to describe Consumer-Driven Contract Testing with Pact.

Pact introduces a (new) way to test the interactions (over the network) between different components without the need to run these components together within the same (shared) environment! In other words, it's an alternative to end-to-end testing, which is called consumer-driven contract testing.

In addition to that, it unambiguously addresses the problematic of test data. I do think that (traditional) tools that are supposed to help performing end-to-end testing, such as service virtualization, leave the question of test data - or more precisely the question of how to set the right test data at the right moment in order to correctly perform the testing of a specific interaction - as "the elephant in the room".

Pact addresses the problematic of test data with the key notion of Provider State. I do see this notion of Provider State as the cornerstone of Pact! But, first, with Pact, you have to understand that one (and only one) interaction - a single request-response pair - is tested at a time, independently. You never (ever) test any sequences of interactions! That means that for each interaction you have to define precisely in which state the provider is, and that's what is called the Provider State.

[To be continued...]

Prerequisites

This scenario has been developed on Windows.

This scenario is using node, curl and jq.

This scenario is also using pact-stub-server and pact_verifier_cli.

Node.js

node --version
v16.13.2

cURL

curl --version
curl 7.79.1 (Windows) libcurl/7.79.1 Schannel
Release-Date: 2021-09-22
Protocols: dict file ftp ftps http https imap imaps pop3 pop3s smtp smtps telnet tftp
Features: AsynchDNS HSTS IPv6 Kerberos Largefile NTLM SPNEGO SSL SSPI UnixSockets

jq

jq --version
jq-1.6

Standalone Pact Stub Server

pact-stub-server --version
pact-stub-server v0.4.4
pact stub server version  : v0.4.4
pact specification version: v3.0.0

Pact Verifier CLI

pact_verifier_cli --version
pact_verifier_cli 0.9.7
pact verifier version   : v0.9.7
pact specification      : v4.0
models version          : v0.2.7

Workflow

1. The consumer gets (typically via a developer portal) the OpenAPI (f.k.a. Swagger) document of an API. For this demo, we will use the "Thingies API", thingies-api.oas2.yaml.

2. The consumer wants to use the "Thingies API" with her/his own test data. For instance when she/he sends the request:

GET localhost:8000/thingies/123

she/he expects to receive the following response:

{
  "id": "123",
  "name": "stuff"
}

3. Therefore, the consumer creates a Pact file that follows her/his scenario/interaction, consumer.pact.json.

Warning. There is, of course, a risk that the consumer does not respect the actual behavior of the API when defining her/his scenario/interaction, that's why there is a verification step afterwards.

4. And then, the consumer can start a Pact Stub Server using the pact-stub-server command with the Pact file:

pact-stub-server --file consumer.pact.json --port 8000

in order to run her/his test script, e.g.consumer.test.cmd:

consumer.test.cmd
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    27  100    27    0     0     27      0  0:00:01 --:--:--  0:00:01   114
  "name": "stuff"
Passed.

Remark. At this stage, this test will obviously work as it is the same consumer that decides, within the test script, what the response should be, but also decides, within the Pact file run by the Pact Stub Server, what the response will be. This test is like a self-fulfilling prophecy! Again, that's why there is a verification step afterwards, during which the provider will verify that the Pact file created by the consumer makes sense or not.

5. Now, the consumer passes her/his Pact file, consumer.pact.json, to the provider so she/he can verify it.

6. The provider runs her/his implementation of the "Thingies API", e.g. provider.app.v1.js:

node provider.app.v1
Provider service is running at localhost:3000...

7. The provider can then verify the consumer Pact file, consumer.pact.json, with the Pact Verifier CLI using the pact_verifier_cli command. This tool will read the Pact file the other way around: it will send the request of the interaction to the actual implementation of the "Thingies API" and check if the actual response corresponds to the response defined by the consumer in the Pact file.

pact_verifier_cli --file consumer.pact.json --hostname localhost --port 3000
  Given has one thingy with '123' as an thingyId
    WARNING: State Change ignored as there is no state change URL provided
09:15:55 [WARN]

Please note:
We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.



Verifying a pact between Thingies Consumer Example and Thingies Provider Example

  get one thingy
    returns a response which
      has status code 200 (FAILED)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (FAILED)
      has a matching body (FAILED)


Failures:

1) Verifying a pact between Thingies Consumer Example and Thingies Provider Example Given has one thingy with '123' as an thingyId - get one thingy
    1.1) has a matching body
           / -> Expected body Present(27 bytes) but was empty
    1.2) has status code 200
           expected 200 but was 404
    1.3) includes header 'Content-Type' with value '"application/json; charset=utf-8"'
           Expected header 'Content-Type' to have value '"application/json; charset=utf-8"' but was ''

There were 1 pact failures

The verification obviously fails as the test data invented by the consumer does not match the test/real data used by the provider.

This is where the Provider States come to the rescue by "allowing you to set up data on the provider by injecting it straight into the data source before the interaction is run, so that it can make a response that matches what the consumer expects." But, how does this "injection" work? No magic, just before sending the request of an interaction the pact_verifier_cli sends the Provider State, i.e. the free text representing the Provider State, to the provider using a POST / endpoint with the following JSON structure:

{
  "action": "setup",
  "params": {},
  "state": "has one thingy with '123' as an thingyId"
}

So, it means that, as a provider you have to manually map (by writing some new specific code) this long string representing the Provider State invented by the consumer with the injection of some specific test data. Something like this, implemented in provider.app.v2.js:

app.post('/', (req, res) => {
  const providerState = req.body.state
  switch(providerState) {
    case "has one thingy with '123' as an thingyId":
      thingies.push({
        id: "123",
        name: "stuff"
      })
      break
    default:
      res.status(404).end()
      return
  }
  res.status(201).end()
})

Warning. This is not production-ready code ;-)

So, you can see that this technique can be error-prone and maybe difficult to scale when a provider has a lot of customers, but this effort needed must be put in perspective with the work needed to maintain and prepare "connected test environment(s)" in order to perform valuable end-to-end testing. As a reminder, the goal of Consumer-Driven Contract Testing with Pact is to remove the need of end-to-end testing!

Running the updated version of implementation of the "Thingies API", provider.app.v2.js:

node provider.app.v2

We can re-run the pact_verifier_cli command specifying the URL of the POST endpoint using the --state-change-url option:

pact_verifier_cli --file consumer.pact.json --hostname localhost --port 3000 --state-change-url http://localhost:3000
  Given has one thingy with '123' as an thingyId
09:18:15 [WARN]

Please note:
We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.



Verifying a pact between Thingies Consumer Example and Thingies Provider Example

  get one thingy
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

Yeah!

References

About

Let's Play with Pact

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published