When you sign a contract, you expect both parties to hold their end of the bargain. The same can be true for testing applications. Contract testing is a way to make sure that services can communicate with each other and that the data shared between the services is consistent with a specified set of rules. In this post, I will guide you through using Joi as a library to create API contracts for services consuming an API.

This tutorial covers:

  • Setting up Joi contract testing framework
  • Using Joi to write contract tests for our API endpoint
  • Using Joi to validate API responses
  • Writing a test to validate an API response against a schema

Prerequisites

To be best positioned to follow along, you will need to have a few things established,

  1. Basic knowledge of JavaScript
  2. Basic knowledge of writing tests
  3. Node.js installed on your system (version >= 11.0)
  4. A CircleCI account
  5. A GitHub account

In this post, I will demonstrate writing contract tests for an open source API endpoint. The endpoint we will be testing monitors the Bitcoin Price Index for different currencies. We will then use a NodeJS app to test the contracts of the returned responses that are provided by the Coinbase API.

Cloning the demo project

To begin the exercise, you will need to clone the demo project. The project is a Node application that is built on top of Express.js. The Coinbuzz application itself has not yet been developed. For this tutorial, we will focus on the contract tests that will be useful to ensure the stability of the application as if it will be developed.

The project will use Coinbase APIs, and for this tutorial, we will use the endpoint https://api.coindesk.com/v1/bpi/currentprice/CNY.json. This endpoint fetches the Bitcoin price index for the Chinese yuan in relation to the United States dollar.

Clone the project by running this command:

git clone git@github.com:CIRCLECI-GWP/coinbuzz-contract-testing.git

Install the dependencies by first navigating to the project folder:

cd coinbuzz-contract-testing;

npm install

There is no need to run the application, since we will only be running our tests in this project.

Why use contract testing

Before we get started, let me give you some background about contract testing. This kind of testing provides confidence that different services work when they are required to. Imagine that an organization has multiple payment services that utilize an Authentication API. The API logs in users into an application with a username and a password. It then assigns them an access token when the log-in operation is successful. Other services like Loans and Repayments require the Authentication API service once users are logged in.

Microservices Dependencies

If the Authentication service changes the way it works and requires email instead of username, the Loans and Repayments services will fail. With contract testing, both the Loans and Repayments services can keep track of the Authentication API by having an expected set of behaviors with the requests made from the service. The services have access to information about when, how, and where failures are happening. There is also information about whether the failures have been caused by an external dependency, which in this case is Authentication.

Setting up the contract test environment

Contract tests are designed to monitor the state of an application and notify testers when there is an unexpected result. Contract tests are most effective when they are used by a tool that relies on the stability of other services. Two examples:

  • A front-end application that relies on the stability of a backend API (like in our project)
  • A microservices environment or an API that relies on another API to process information

Testing endpoints from CoinDesk will help us understand the Bitcoin Price Index for different currencies on different days. To learn more, review app.js in our cloned application.

Before you can begin testing the API, you need an understanding of its structure. That knowledge will help you write your contracts. First, use the browser to make a simple GET request to this URL:

https://api.coindesk.com/v1/bpi/currentprice/CNY.json

This request fetches the current price of Bitcoin over a certain period of time. It then shows the conversion in both USD (United States Dollars) and the CNY (Chinese Yuan).

Bitcoin Price Index browser response

While the response object might look a little bit intimidating at first, we can work through it by breaking it down into individual objects using Joi, which we will do in the next section of this tutorial. But before we do that, let us first set up our CircleCI pipeline.

Setting up a project on CircleCI

If you cloned the sample project repository, it is already initialized and set up in git. It can be helpful to understand how our project is integrated with CircleCI. To set up CircleCI, initialize a GitHub repository in your project by running the command:

git init

Next, create a .gitignore file in the root directory. Inside the file, add node_modules, to ignore npm-generated modules from being added to your remote repository. Add a commit and then push your project to GitHub.

Log into CircleCI and navigate to Projects. All the repositories associated with your GitHub username or your organization are listed, including the one you want to set up in CircleCI. In this case it is coinbuzz-contract-testing.

adding-a-project

On the Projects dashboard, click the Set Up Project button. Then, click Use Existing Config.

start-building-page

When prompted, click Start Building. The pipeline fails, which is as expected. We still need to add our customized .circleci/config.yml configuration file to GitHub before the project will build properly.

start-building-prompt

Writing the CI pipeline configuration

Once we have setup CircleCI pipeline, it is time to add CircleCI to our local project. Start by creating a folder named .circleci in the root directory. Inside the folder, create a config.yml file. Now add configuration details:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: cimg/node:10.16.3
    steps:
      - checkout
      - run:
          name: update npm
          command: "npm install -g npm@5"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: install dependencies
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: run coinbuzz contract-tests
          command: npm test
      - store_artifacts:
          path: ~/repo/coinbuzz

In this configuration, CircleCI uses a Node Docker image, pulled from the environment. It then updates npm package manager. Next, if the cache exists, it is restored. Application dependencies are updated only when a change has been detected with save-cache. The Coinbuzz tests are run and cached items are stored in the artifacts coinbuzz directory.

Pushing your changes to GitHub will automatically start the building process on CircleCI. Because we do not have any tests yet, our pipeline will fail (again). That is ok, because we will re-run this after we add the tests. Even without tests, details for the pipeline are available to review.

Creating the Joi contracts

Joi is a tool that makes it possible to analyze objects and break them into chunks that can be validated. Joi views a response as a single object with two key values of time and bpi. Each value is of a particular data type. By breaking down the response, Joi can to analyze the response and create assertions of either success or failure based on the defined schema contracts.

To install the joi package for our project, open a Terminal window and run this command:

npm install joi

Great! We have started the process of creating our Joi contracts. This section will cover how to create just a part of the object, which can then be extended to create other sections of the response. We will use a sample code block that shows how to break down the object into chunks. We can test the currencyObject and later re-use it for either USD or CNY.

const currencyObject = Joi.object({
        code: Joi.string().required(),
        symbol: Joi.string().optional(),
        rate: Joi.string().required(),
        description: Joi.string().required(),
        rate_float: Joi.number().required()
  }).required();

In this code block, we are taking apart the currency object and telling Joi that in our response, we are expecting the currency object to have the key items code, symbol, rate, description, and rate_float. Also in the contract, the state of the items in our object are described, including whether they should be optional() or required(). In this case we have added the symbol key as optional because we want to reuse the currencyObject with other responses that have a similar structure.

Partial API response and JOI contract

The currency object matches side-by-side to both the currency responses for the USD and the CNY. It can be re-used because the currencyObject contract matches the criteria for both the CNY and the USD response objects. The full BPIContract is integrated with the currencyObject in a way that can be re-used multiple times. The larger context of the full contract of the response is from CoinDesk CNY request.

In this section, we have covered creating Joi contracts and defining the properties of responses that need to be verified by the contract schema. Keep in mind that to “tighten” schemas, you should define whether the contract schema values are required or optional. This creates a boundary on whether to throw an error on failure or to ignore it.

Handling Joi contract errors

When writing contracts, it is important to handle the errors that may arise from issues with the responses. You will need to know whether the responses are consistent every time you execute your tests, and when there is a contract schema failure. Understanding potential contract failures will help you learn how to tweak your applications to handle the failures.

To handle the errors, create a directory and call it lib. Add two files to the directory:

  1. One for the request helper that helps make the API requests using axios
  2. Another file for the schemaValidation() method to validate responses against the defined schemas

side by side lib files

The schema validation file verifies that that the response provided and the contract are consistent. If they are not, it results in an error. The next code block shows where we are using a method to verify the received response against the defined Joi contract schema.

 async function schemaValidation(response, schema) {
    if (!response || !schema) throw new Error('An API response and contract are required');
    const options = { abortEarly: false };
    try {
        const value = await schema.validateAsync(response, options);
        return value
    }
    catch (err) {
        throw new Error(err);
    }
}

module.exports = {
    schemaValidation
}

The schema.validateAsync() Joi method is responsible for validating the responses against the created schema. In this case, we already have a schema created in the file contracts/bpi-contracts.js. In the next section, we will verify that this method works by writing a test and passing in both the schema and the received API response from a Coinbase API.

Writing tests and assertions

You have successfully written your contracts and a method to validate them against API responses while checking for inconsistencies. Our next step is to write a test that uses this method. To test the API using our schema, you will need to install jest, a JavaScript testing framework, and axios, a JavaScript request-making framework.

npm install jest axios

We will use Jest to run our tests, and axios to make the API requests to the Coinbase endpoints. To make this possible, add the test command to run the tests in the package.json file:

"scripts": {
    "start": "node ./bin/www",
    "test": "jest"
  },

Adding the command in the scripts section enables Jest to scan our project for any file that has either a .spec or .test extension. When one is discovered, Jest treats them as test files and runs them.

Now, create a test inside the contract-tests directory. This test calls the schema validation method after making an API request.

const { getData } = require('../lib/request-helper')
const { schemaValidation } = require('../lib/validateContractSchema');
const { BPIContract } = require('../contracts/bpi-contracts');

describe('BPI contracts', () => {
    test('CNY bpi contract schema check', async () => {
        const response = await getData({
            url: 'https://api.coindesk.com/v1/bpi/currentprice/CNY.json' });
        return schemaValidation(response, BPIContract);
    });
});

This call uses the getData() method to make the request. The getData() method uses axios to make a call to the Coinbase API, then uses the response to verify the API response against the schema validation method schemaValidation(). This already has our Joi schema definitions for the response received from our endpoint. Run our test and validate that it works by running:

npm test 

Check your terminal for some good news.

successful test run

Voila! Our test passes.

Hold on, though. It is still too early to host a party. We need to verify that the test also handles situations when the contracts are not correct. In a previous contract schema, we used the field symbol: Joi.string().optional() in the currencyObject. We can change the Joi expectation to required() and find out if Joi will create errors. Edit the currencyObject to match this:

const currencyObject = Joi.object({
        code: Joi.string().required(),
        symbol: Joi.string().required(),
        rate: Joi.string().required(),
        description: Joi.string().required(),
        rate_float: Joi.number().required()
    }).required();

After re-running our tests, we are indeed presented with an error.

failed test run

Joi was able to capture the error, and now expects the symbol object item to be required() and not optional() in the currency object. The response from the API does not have the symbol as part of the response, and Joi rejects the API response. The response does not meet the criteria of the contract.

We can now declare this a success. We have been able to show that our contract schema works. A modification of a response will result in a failure which we will be alerted about.

Verifying pipeline success

Now that the tests are working and the CI pipeline is set up, you can add all the files to git and push them to Github remote repository. Our CircleCI pipeline should kick in and run our tests automatically. To observe the pipeline execution, go to the CircleCI dashboard and click the project name (coinbuzz-contract-testing).

To review the build status, select the build from the CircleCI dashboard. You will be able to review the status of every step as defined in your CircleCI configuration file.

build-status

Everything appears green: victory!

Conclusion

In this tutorial, you have created a contract schema for an API response, a method to handle errors in the schema, and a test to verify that the contract schema works. I hope have been able to demonstrate how easy it is to set up contract tests for APIs or frontend applications that depend on the availability of external services. It can be possible to forget about undetected dependency flakiness when you can pinpoint a failing service to the accuracy of a changed response object.


Waweru Mwaura is a Software Engineer and a lifelong-learner who specializes in Quality Engineering. He is an author at packt and enjoys reading about engineering, finance, and technology. You can read more about him on his web profile.