DEV Community

Kentaro Wakayama
Kentaro Wakayama

Posted on • Updated on • Originally published at codersociety.com

How To: Contract Testing for Node.js Microservices with Pact

In this article, you'll learn more about contract testing and how to use Pact to verify and ensure your Node.js microservices' API compatibility.

Alt Text

This article was originally published at Coder Society

Ensuring API compatibility in distributed systems

The use of microservices is growing in popularity for good reasons.

They allow software teams to develop, deploy, and scale software independently to deliver business value faster.

Large software projects are broken down into smaller modules, which are easier to understand and maintain.

While the internal functionality of each microservice is getting simpler, the complexity in a microservice architecture is moved to the communication layer and often requires the integration between services.

However, in microservice architectures, you often find service to service communication, leading to increased complexity in the communication layer and the need to integrate other services.

Figure 1: Distributed systems at Amazon and Netflix

Traditional integration testing has proven to be a suitable tool to verify the compatibility of components in a distributed system. However, as the number of services increases, maintaining a fully integrated test environment can become complex, slow, and difficult to coordinate. The increased use of resources can also become a problem, for example when starting up a full system locally or during continuous integration (CI).

Contract testing aims to address these challenges -- let's find out how.

What is contract testing?

Contract testing is a technique for checking and ensuring the interoperability of software applications in isolation and enables teams to deploy their microservices independently of one another.

Contracts are used to define the interactions between API consumers and providers. The two participants must meet the requirements set out in these contracts, such as endpoint definitions and request and response structures.

Figure 2: A contract that defines a HTTP GET interaction

What is consumer-driven contract testing?

Consumer-driven contract testing allows developers to start implementing the consumer (API client) even though the provider (API) isn't yet available. For this, the consumer writes the contract for the API provider using test doubles (also known as API mocks or stubs). Thanks to these test doubles, teams can decouple the implementation and testing of consumer and provider applications so that they're not dependent on each other. Once the provider has verified its structure against the contract requirements, new consumer versions can be deployed with confidence knowing that the systems are compatible.

Figure 3: Consumer-driven contract testing

What is Pact?

Pact is a code-first consumer-driven contract testing tool. Consumer contracts, also called Pacts, are defined in code and are generated after successfully running the consumer tests. The Pact files use JSON format and are used to spin up a Pact Mock Service to test and verify the compatibility of the provider API.

The tool also offers the so-called Pact Mock Provider, with which developers can implement and test the consumer using a mocked API. This, in turn, accelerates development time, as teams don't have to wait for the provider to be available.

Figure 4: Pact overview

Pact was initially designed for request/response interactions and supports both REST and GraphQL APIs, as well as many different programming languages. For Providers written in languages that don't have native Pact support, you can still use the generic Pact Provider Verification tool.

Try out Pact

Why don't we test things ourselves and see how consumer-driven contract testing with Pact actually works? For this, we use Pact JS, the Pact library for JavaScript, and Node.js. We've already created a sample repository containing an order API, which returns a list of orders. Let's start by cloning the project and installing the dependencies:

$ git clone https://github.com/coder-society/contract-testing-nodejs-pact.git

$ cd contract-testing-nodejs-pact

$ npm install
Enter fullscreen mode Exit fullscreen mode

Writing a Pact consumer test

We created a file called consumer.spec.js to define the expectedinteractions between our order API client (consumer) and the order API itself (provider). We expect the following interactions:

  • HTTP GET request against path /orders which returns a list of orders.
  • The order response matches a defined structure. For this we use Pact's Matchers.
const assert = require('assert')
const { Pact, Matchers } = require('@pact-foundation/pact')
const { fetchOrders } = require('./consumer')
const { eachLike } = Matchers

describe('Pact with Order API', () => {
  const provider = new Pact({
    port: 8080,
    consumer: 'OrderClient',
    provider: 'OrderApi',
  })

  before(() => provider.setup())

  after(() => provider.finalize())

  describe('when a call to the API is made', () => {
    before(async () => {
      return provider.addInteraction({
        state: 'there are orders',
        uponReceiving: 'a request for orders',
        withRequest: {
          path: '/orders',
          method: 'GET',
        },
        willRespondWith: {
          body: eachLike({
            id: 1,
            items: eachLike({
              name: 'burger',
              quantity: 2,
              value: 100,
            }),
          }),
          status: 200,
        },
      })
    })

    it('will receive the list of current orders', async () => {
      const result = await fetchOrders()
      assert.ok(result.length)
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

Run the Pact consumer tests using the following command:

$ npm run test:consumer

> contract-testing-nodejs-pact@1.0.0 test:consumer /Users/kentarowakayama/CODE/contract-testing-nodejs-pact
> mocha consumer.spec.js

[2020-11-03T17:22:44.144Z]  INFO: pact-node@10.11.0/7575 on coder.local:
    Creating Pact Server with options:
    {"consumer":"OrderClient","cors":false,"dir":"/Users/kentarowakayama/CODE/contract-testing-nodejs-pact/pacts","host":"127.0.0.1","log":"/Users/kentarowakayama/CODE/contract-testing-nodejs-pact/logs/pact.log","pactFileWriteMode":"overwrite","port":8080,"provider":"OrderApi","spec":2,"ssl":false}

  Pact with Order API
[2020-11-03T17:22:45.204Z]  INFO: pact@9.13.0/7575 on coder.local:
    Setting up Pact with Consumer "OrderClient" and Provider "OrderApi"
        using mock service on Port: "8080"
    when a call to the API is made
[{"id":1,"items":[{"name":"burger","quantity":2,"value":100}]}]
      ✓ will receive the list of current orders
[2020-11-03T17:22:45.231Z]  INFO: pact@9.13.0/7575 on coder.local: Pact File Written
[2020-11-03T17:22:45.231Z]  INFO: pact-node@10.11.0/7575 on coder.local: Removing Pact process with PID: 7576
[2020-11-03T17:22:45.234Z]  INFO: pact-node@10.11.0/7575 on coder.local:
    Deleting Pact Server with options:
    {"consumer":"OrderClient","cors":false,"dir":"/Users/kentarowakayama/CODE/contract-testing-nodejs-pact/pacts","host":"127.0.0.1","log":"/Users/kentarowakayama/CODE/contract-testing-nodejs-pact/logs/pact.log","pactFileWriteMode":"overwrite","port":8080,"provider":"OrderApi","spec":2,"ssl":false}

  1 passing (1s)
Enter fullscreen mode Exit fullscreen mode

The consumer tests generate a Pact contract file named "orderclient-orderapi.json" in the "pacts" folder, which looks like this:

{
  "consumer": {
    "name": "OrderClient"
  },
  "provider": {
    "name": "OrderApi"
  },
  "interactions": [
    {
      "description": "a request for orders",
      "providerState": "there are orders",
      "request": {
        "method": "GET",
        "path": "/orders"
      },
      "response": {
        "status": 200,
        "headers": {
        },
        "body": [
          {
            "id": 1,
            "items": [
              {
                "name": "burger",
                "quantity": 2,
                "value": 100
              }
            ]
          }
        ],
        "matchingRules": {
          "$.body": {
            "min": 1
          },
          "$.body[*].*": {
            "match": "type"
          },
          "$.body[*].items": {
            "min": 1
          },
          "$.body[*].items[*].*": {
            "match": "type"
          }
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "2.0.0"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Verifying the consumer pact against the API provider

We can now use the generated Pact contract file to verify our order API. To do so, run the following command:

$ npm run test:provider

> contract-testing-nodejs-pact@1.0.0 test:provider /Users/kentarowakayama/CODE/contract-testing-nodejs-pact
> node verify-provider.js

Server is running on http://localhost:8080
[2020-11-03T17:21:15.038Z]  INFO: pact@9.13.0/7077 on coder.local: Verifying provider
[2020-11-03T17:21:15.050Z]  INFO: pact-node@10.11.0/7077 on coder.local: Verifying Pacts.
[2020-11-03T17:21:15.054Z]  INFO: pact-node@10.11.0/7077 on coder.local: Verifying Pact Files
[2020-11-03T17:21:16.343Z]  WARN: pact@9.13.0/7077 on coder.local: No state handler found for "there are orders", ignoring
[2020-11-03T17:21:16.423Z]  INFO: pact-node@10.11.0/7077 on coder.local: Pact Verification succeeded.
Enter fullscreen mode Exit fullscreen mode

The code to verify the provider can be found in verify-pact.js and looks like this:

const path = require('path')
const { Verifier } = require('@pact-foundation/pact')
const { startServer } = require('./provider')

startServer(8080, async (server) => {
  console.log('Server is running on http://localhost:8080')

  try {
    await new Verifier({
      providerBaseUrl: 'http://localhost:8080',
      pactUrls: [path.resolve(__dirname, './pacts/orderclient-orderapi.json')],
    }).verifyProvider()
  } catch (error) {
    console.error('Error: ' + error.message)
    process.exit(1)
  }

  server.close()
})
Enter fullscreen mode Exit fullscreen mode

This starts the API server and runs the Pact Verifier. After successful verification, we know that the order API and the client are compatible and can be deployed with confidence.

Wrapping up

By now you should have a good understanding of contract testing and how consumer-driven contract testing works. You also learned about Pact and how to use it to ensure the compatibility of your Node.js microservices.

To avoid exchanging the Pact JSON files manually, you can use Pact Broker to share contracts and verification results. This way, Pact can be integrated into your CI/CD pipeline -- we will talk more about this in a future blog post.

Visit the Pact documentation to learn more about Pact and consumer-driven contract testing for your microservices.

For more articles like this, visit our Coder Society Blog.

For our latest insights and updates, you can follow us on LinkedIn.

Top comments (0)