Skip to main content

Step 2 - Create consumer Pact test

Learning Objectives

StepTitleConcept CoveredLearning objectivesFurther Reading
step 2Write a Pact test for our consumerConsumer side pact test
  • Understand basic Consumer-side Pact concepts
  • Understand "Matchers" to avoid test data brittleness
  • Demonstrate that Pact tests are able to catch a class of integration problems

How Message Pact works

Pact is a consumer-driven contract testing tool, which is a fancy way of saying that the API Consumer writes a test to set out its assumptions and needs of its API Provider(s). By unit testing our API client with Pact, it will produce a contract that we can share to our Provider to confirm these assumptions and prevent breaking changes.

The process looks like this on the consumer side:

diagram

The process looks like this on the provider (producer) side:

diagram

  1. The consumer writes a unit test of its behaviour using a Mock provided by Pact.
  2. Pact writes the interactions into a contract file (as a JSON document).
  3. The consumer publishes the contract to a broker (or shares the file in some other way).
  4. Pact retrieves the contracts and replays the requests against a locally running provider.
  5. The provider should stub out its dependencies during a Pact test, to ensure tests are fast and more deterministic.

Writing our consumer test

In this section we will look at 1 & 2, writing the unit test which will generate the contract file we can share with our provider.

There are code snippets, below the following steps, that you can use to guide you through the process.

It will consist of the following steps:

  1. The target of our test, our Product Event Handler.
    • In most applications, some form of transactionality exists and communication with a MQ/broker happens.
    • It's important we separate out the protocol bits from the message handling bits, so that we can test that in isolation.
  2. Import Pact DSL for your language of choice
  3. Setup Pact Message Consumer Constructor, which will vary slightly depending on your implementation. Here you can setup the name of the consumer/provider pair for the test, and any required pact options
  4. Setup the expectations for the consumer
    1. The description for the event
      1. Used in the provider side verification to map to a function that will produce this message
    2. The contents of the message, we expect to receive
      1. It is advisable to use your domain objects, to build up the contents of the message, as this will help you catch any changes in your domain model, and ensure your tests are updated accordingly.
    3. Pact matchers can be applied, to allow for flexible verification, based on applied matchers.
      1. Your Pact DSL should provide a function to generate a concrete object, from an object that has matchers applied. It is known as the reify function, but may have aliases. This is useful as you can use the object your message will receive in your test, but powerful matchers can be applied to ensure your test is flexible during provider side verification.
      2. For more details on matching, see our documentation
    4. Setup any required metadata
      1. A consumer may require additional data, which does not form part of the message content. This could be any that can be encoded in a key value pair, that is serialisable to json. In our case, it is the kafka topic our consumer will subscribe to.
  5. Pact will send the message to your message handler. If the handler returns a successful promise, the message is saved, otherwise the test fails. There are a few key things to consider:
    • The actual request body that Pact will send, will be contained within a message object along with other context, so the body must be retrieved via content attribute. Metadata can be accessed via the metadata attribute.
    • All handlers to be tested generally must be of the shape (m: Message) => Promise<any> - that is, they must accept a Message and return a Promise. This is how we get around all of the various protocols, and will often require a lightweight adapter function to convert it, some language DSL's will provide this for you.
      • In the JavaScript case, we wrap the actual productEventHandler with a convenience function asynchronousBodyHandler provided by Pact, which Promisifies the handler and extracts the contents.
      • If one is not provided, you will need to write your own, which is a simple function that takes the message provided by the Pact framework, as setup in your test, converts it to the correct type and calls the handler under test, returning if the message is processed successfully or throwing if unsuccessful.

Code Snippets

in consumer-js-kafka/src/product/product.handler.pact.test.js:

// 1. The target of our test, our Product Event Handler
const productEventHandler = require("./product.handler");

// 2. Import Pact DSL for your language of choice
const {
MatchersV3,
MessageConsumerPact,
asynchronousBodyHandler,
} = require("@pact-foundation/pact");
const { like, regex } = MatchersV3;

const path = require("path");

describe("Kafka handler", () => {
// 3. Setup Pact Message Consumer Constructor
// specifying consumer & provider naming
// and any required options
const messagePact = new MessageConsumerPact({
consumer: "pactflow-example-consumer-js-kafka",
dir: path.resolve(process.cwd(), "pacts"),
pactfileWriteMode: "update",
provider: "pactflow-example-provider-js-kafka",
logLevel: process.env.PACT_LOG_LEVEL ?? "info",
});

describe("receive a product update", () => {
it("accepts a product event", () => {
// 4. Arrange - Setup our message expectations
return (
messagePact
// The description for the event
// Used in the provider side verification to map to
// a function that will produce this message
.expectsToReceive("a product event update")
// The contents of the message, we expect to receive
// Pact matchers can be applied, to allow for flexible
// verification, based on applied matchers.
.withContent({
id: like("some-uuid-1234-5678"),
type: like("Product Range"),
name: like("Some Product"),
version: like("v1"),
event: regex("^(CREATED|UPDATED|DELETED)$", "UPDATED"),
})
// Setup any required metadata
// A consumer may require additional data, which does not
// form part of the message content. This could be any
// that can be encoded in a key value pair, that is
// serialisable to json. In our case, it is the kafka
// topic our consumer will subscribe to
.withMetadata({
contentType: "application/json",
kafka_topic: "products",
})
// 5. Act
// Pact provides a verification function where the message
// content, and metadata are made available, in order to process
// and pass to your system under test, our Product Event Handler.
//
// Some Pact DSL's will provide body handlers, as convenience functions
//
.verify(asynchronousBodyHandler(productEventHandler))
);
});
});
});

Running the test

You can now run the test.

> product-service@1.0.0 test
> jest --testTimeout 30000


RUNS src/product/product.handler.pact.test.js
PASS src/product/product.handler.pact.test.js
● Console

console.log
received product: {
event: 'UPDATED',
id: 'some-uuid-1234-5678',
name: 'Some Product',
type: 'Product Range',
version: 'v1'
}

at log (src/product/product.handler.js:5:11)

console.log
received product event: UPDATED

at log (src/product/product.handler.js:6:11)

PASS src/product/product.repository.test.js

Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.601 s, estimated 1 s

Examine the generated Pact file

Take a look at the pact directory, at the generated contract.

{
"consumer": {
"name": "pactflow-example-consumer-js-kafka"
},
"messages": [
{
"contents": {
"event": "UPDATED",
"id": "some-uuid-1234-5678",
"name": "Some Product",
"type": "Product Range",
"version": "v1"
},
"description": "a product event update",
"matchingRules": {
"body": {
"$.event": {
"combine": "AND",
"matchers": [
{
"match": "regex",
"regex": "^(CREATED|UPDATED|DELETED)$"
}
]
},
"$.id": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
},
"$.name": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
},
"$.type": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
},
"$.version": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
}
},
"metadata": {}
},
"metadata": {
"contentType": "application/json",
"kafka_topic": "products"
}
}
],
"metadata": {
"pact-js": {
"version": "13.1.4"
},
"pactRust": {
"ffi": "0.4.22",
"models": "1.2.3"
},
"pactSpecification": {
"version": "3.0.0"
}
},
"provider": {
"name": "pactflow-example-provider-js-kafka"
}
}

Breaking the test

Your handler should throw an error, if it is unable to process the message. Try commenting out a value such as the event type, in your Pact expectations and re-run your test.

Depending on how your code is structured, it should throw an error, if required fields aren't present. If it doesn't, you may need to add some additional validation to your handler.

Pact aids in test-driven development by helping you mock out the expected behaviour of your provider, and ensuring that your consumer is correctly implemented, before our provider is built, this can help you catch a class of integration problems early, by applying pressure to the design of your test.


```sh
> product-service@1.0.0 test
> jest --testTimeout 30000


RUNS src/product/product.handler.pact.test.js
FAIL src/product/product.handler.pact.test.jse library successfully found, and the correct version
Console

console.log
received product: { id: 'some-uuid-1234-5678' }

at log (src/product/product.handler.js:5:11)

console.log
received product event: undefined

at log (src/product/product.handler.js:6:11)

Kafka handler › receive a product update › accepts a product event

Unable to process event

19 | );
20 | }
> 21 | throw new Error("Unable to process event")
| ^
22 | };
23 |
24 | module.exports = handler;

at handler (src/product/product.handler.js:21:9)
at node_modules/@pact-foundation/src/messageConsumerPact.ts:254:34
at MessageConsumerPact.Object.<anonymous>.MessageConsumerPact.verify (node_modules/@pact-foundation/src/messageConsumerPact.ts:187:12)
at Object.verify (src/product/product.handler.pact.test.js:35:10)

PASS src/product/product.repository.test.js

Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 0.678 s, estimated 1 s

Update your test, and re-run it, so your Pact file is up-to-date.

Step 3

We can now move onto step 3, where we will build out our provider code.

Move on to step 3