Skip to content

Consumer Testing

Anudeep edited this page Apr 19, 2020 · 5 revisions

Consumer Testing

Consumer testing is the first step in contract testing. This testing helps in building/generating contracts for consumers with its external providers.

In pactum, consumer tests are written as a wrapper around the component testing.

Table of contents

Getting Started

Before getting started with consumer testing, get familiar with component testing

Example Scenario

Here we have an example describing pactum consumer tests between a consumer (order-service) and its provider (product-service).

To make things simple, let's assume order-service has a single order with id 1. When requested for this order details, the order-service will fetch corresponding product details from its provider product-service.

Order Service Code

const fetch = require('node-fetch');
const express = require('express');
const app = express();

const PRODUCT_SERVICE_URL = process.env.PRODUCT_SERVICE_URL || 'http://my-shop.com';

app.get('/api/orders/1', (req, res) => {
  // order with id "1" includes a product with id "190"
  const productId = 190;
  // fetches product details with id "190"
  fetch(`${PRODUCT_SERVICE_URL}/api/products?id=${productId}`)
    .then(product => {
      const order = {
        id: 1,
        product: {
          id: productId,
          name: product.name
          price: product.price
        }
      }
      // responds with order details
      res.send(order);
    });
});

app.listen(3000, () => console.log(`Example app listening on port ${port}!`))

Requesting order details will result in the following response

{
  "id": 1,
  "product": {
    "id": 190,
    "name": "book",
    "price": 321.12
  }
}

Component Test for Order Service

A general component test for order service might look like:

const pactum = require('pactum');

it('should fetch order details', async () => {
  await pactum
    .get('http://localhost:3000/api/orders/1')
    .expectStatus(200)
    .expectJson({
      "id": 1,
      "product": {
        "id": 190,
        "name": "book",
        "price": 321.12
      }
    })
    .toss();
});

As described earlier, during component testing the order-service will be talking to a mock version of product-service.

Consumer Test for Order Service

During consumer testing with pactum, you don't need to spin up a mock server & train it to behave like product service. Pactum comes with a mock server where you can train your service on the fly to react in a specific way when a specific request is received.

As you observed in consumer code, the order-service will be making the following request on product-service.

Request:
--------------------------
Method  -   GET
Path    -   /api/products
Query   -   id = 1

Response
--------------------------
{
  "id": 190,
  "name": "book",
  "price": 321.12
}

Pactum allows you to add this interaction to the mock server before running a consumer test case. And also it automatically removes all interactions after the execution of a test case.

You can add a Pact Interaction by

const pactum = require('pactum');

pactum.addPactInteraction({
  provider: 'product-service',
  state: 'there is product with id 190',
  uponReceiving: 'a request for product',
  withRequest: {
    method: 'GET',
    path: '/api/products',
    query: {
      id: 190
    }
  },
  willRespondWith: {
    status: 200,
    headers: {
      'content-type': 'application/json'
    },
    body: {
      "id": 190,
      "name": "book",
      "price": 321.12
    }
  }
});

Now, while running order service you need to update order-service to talk with pactum's mock server. By default pactum mock server runs on port 9393.

A consumer test might look like

const pactum = require('pactum');

before(async () => {
  // sets the name of the consumer
  pactum.pact.setConsumerName('order-service');
  // starts the mock server on port 9393
  await pactum.mock.start();
});

it('should fetch order details', async () => {
  await pactum
    .addPactInteraction({
      provider: 'product-service',
      state: 'there is product with id 190',
      uponReceiving: 'a request for product',
      withRequest: {
        method: 'GET',
        path: '/api/products',
        query: {
          id: 190
        }
      },
      willRespondWith: {
        status: 200,
        headers: {
          'content-type': 'application/json'
        },
        body: {
          "id": 190,
          "name": "book",
          "price": 321.12
        }
      }
    })
    .get('http://localhost:3000/api/orders/1')
    .expectStatus(200)
    .expectJson({
      "id": 1,
      "product": {
        "id": 190,
        "name": "book",
        "price": 321.12
      }
    })
    .toss();
});

after(async () => {
  // saves the contract file in ./pacts/ folder
  pactum.pact.save();
  // stops the mock server
  await pactum.mock.stop();

});

A real application/service might be much more complex with a lot of moving parts. The service under test might be talking to multiple external services. Pactum allows you to specify multiple interactions in a single test case. You can add multiple pact & mock interactions in a single spec.

If anyone of the interaction is not exercised, the test will fail. At the end of the test case, all these interactions will be removed from the mock server.

await pactum
  .addPactInteraction(`GET_PRODUCT_DETAILS_WITH_ID_1`)
  .addPactInteraction(`POST_AUDIT_DETAILS`)
  .addMockInteraction(`GET_DELIVERY_DETAILS`)
  .get('http://localhost:3000/api/orders/1')
  .expectStatus(200)
  .expectJson({
    "id": 1,
    "product": {
      "id": 190,
      "name": "book",
      "price": 321.12
    }
  })
  .toss();

Publishing Contracts to Pact Broker

The final step of consumer testing is to publish the contracts (pact files) to a shared location like pact-broker.

await pactum.pact.publish({
  pactFilesOrDirs: ['./pacts/'],
  pactBroker: 'http://localhost:9292',
  consumerVersion: '1.2.3'
});

API

pactum.mock

setDefaultPort

Type: Function

Sets the port of the mock server to start. The default port of the server is 9393.

This function should be called before starting the server.

pactum.mock.setDefaultPort(3333);

start

Type: Function

Starts the mock server on the default/custom port.

pactum.mock.start();

stop

Type: Function

Stops the mock server.

pactum.mock.stop();

pactum.pact

setConsumerName

Type: Function

sets the name of the consumer

pactum.pact.setConsumerName('order-service');

save

Type: Function

saves all the contracts(pact files) in the specified directory

pactum.pact.save();

setPactFilesDirectory

Type: Function

sets directory for saving pact files

This should be called before save()

pactum.pact.setPactFilesDirectory('/path/for/saving/pact-files');

publish

Type: Function

publish pact files to pact broker

This should be called after save()

pactum.pact.publish({
  pactFilesOrDirs: ['./pacts/'],
  pactBroker: 'http://pact-broker',
  pactBrokerUsername: 'username',
  pactBrokerPassword: '********',
  consumerVersion: '1.2.22',
  tags: ['QA', 'DEV']
});

Contract Testing Provider Verification