Building a bulletproof JavaScript API Clients with nock - JSCasts episode 15

In this episode, I will tell you how to write bulletproof API clients in javascript using Test Driven Development. We will use axios - a popular library performing API calls. But a more interesting part will be unit tests, where we will use Nock - server mocking and expectation library.

As usual, code for the cast is available on GitHub. If you prefer watching instead of reading here is a youtube video:

JavaScript API Client

Let’s have a look at our example project. In package.json file there is just one test command. We’ve already set up mocha to test all the files with extension .spec.js.

{
    "scripts": {
        "test": "mocha --recursive ./src/**/*.spec.js"
    }
}

And there are 2 empty files. One for tests and one for the actual implementation.

If you don’t know how to set up a test environment - check out episode 2.

We will create a simple GitHub API client which will fetch issues for a given repository.

Writing the first test

We are doing TDD - so let’s write the test first.

Let’s use expect assertion from chai library, which I already installed. And import a Client library we will soon create.

Next, start with the describe section, name it “Client”, and describe our constructor. Basically, it should create API property which will be axios instance.

When we go to the axios documentation page we see that there is a “create factory” method and axios instance has a get method - so let’s test if it exists

Go back to the editor, And create the first test case. Name it accordingly. And create the client instance. Of course, the first letter of the class name should be capitalised so let’s fix that. Now, write the expect block.

To test if the get method exists we will use .itself.respondTo assertion.

We will add repoName as a parameter to the constructor so all consecutive API requests will be done for this repo. And set this to JSCasts-episodes/ep1-jsdoc in tests

The Client should store this property so let’s test that as well.

This is how it looks:

// src/client.spec.js
const { expect } = require('chai')

const Client = require('./client')
const REPO_NAME = 'JSCasts-episodes/ep1-jsdoc'

describe('Client', function () {
  describe('#constructor', function () {
    it('initializes axios instance', function () {
      const client = new Client({ repoName: REPO_NAME })
      expect(client.api).itself.respondTo('get')
      expect(client.repoName).to.equal(REPO_NAME)
    })
  })
})

Run tests - and they fail - because there is nothing in client.js file. So let’s implement our constructor.

Build the API Client with axios

Add axios first. And create a new class. Define the constructor with the repoName as a parameter. Now, initialize the API property using the axios.create function.

As for the baseURL we will use GitHub API - but put it to the const. We also have to store repoName for further usage. Let’s do that. And export the class

This is how it looks:

// src/client.js
const axios = require('axios')

const GITHUB_API = 'https://api.github.com/'

class Client {
  constructor({ repoName }) {
    this.api = axios.create({
      baseURL: GITHUB_API
    })

    this.repoName = repoName
  }
}

module.exports = Client

Now rerun the test - and it works. So we performed our first TDD loop.

Recording API responses with Nock

Next thing will be to implement the method called getIssues, which will fetch all the issues for a given repo.

In order to test that we will use nock: HTTP server mocking and expectations library. Install it first.

yarn add --dev nock

Now let’s enter the node console to perform some test calls with axios,

node --experimental-repl-await

We will use --experimental-repl-await flag. If you want to know more about REPL - check out episode 11.

Load axios and fetch issues for a JSCasts-episodes/ep1-jsdoc repository

let issues = await a.get('https://api.github.com/repos/JSCasts-episodes/ep1-jsdoc/issues')

Let’s take a look at the response.

issues.data
# response object

And its length.

issues.data.length
# 1

We see that there is 1 issue.

Ok - so now let’s incorporate nock to our project. Require it first.

const nock = require(‘nock’)

Let’s use it to record the request to GitHub API  - so that we can use it in our tests later on.

nock.recorder.rec()

Now we can perform exactly the same command, but nock prints the code which we can use as a response in tests. Copy it to a separate file to spec folder. Call it nocks.

Export the getIssues function and, inside, return the copied code. I will turn on the wrap text in the editor.

This nock command specifies that all the get requests passed to this url should be intercepted and returned as a response with code 200 and this body (which is the actual list with 1 issue).

Let’s move this out to a separate json file. Name it getIssuesResponse,

Create the file and paste the code there. Prettify it and we see that there is an exact 1 issue in the JSON form returned by the API.

[
	{
		"url": "https://api.github.com/repos/JSCasts-episodes/ep1-jsdoc/issues/1",
		"repository_url": "https://api.github.com/repos/JSCasts-episodes/ep1-jsdoc",
		"labels_url": "https://api.github.com/repos/JSCasts-episodes/ep1-jsdoc/issues/1/labels{/name}",
		"comments_url": "https://api.github.com/repos/JSCasts-episodes/ep1-jsdoc/issues/1/comments",
		"events_url": "https://api.github.com/repos/JSCasts-episodes/ep1-jsdoc/issues/1/events",
		"html_url": "https://github.com/JSCasts-episodes/ep1-jsdoc/issues/1",
		"id": 522470412,
		"node_id": "MDU6SXNzdWU1MjI0NzA0MTI=",
		"number": 1,
		"title": "example issue no.1",
		"user": {
			"login": "wojtek-krysiak",
			"id": 825861,
			"node_id": "MDQ6VXNlcjgyNTg2MQ==",
			"avatar_url": "https://avatars3.githubusercontent.com/u/825861?v=4",
			"gravatar_id": "",
			"url": "https://api.github.com/users/wojtek-krysiak",
			"html_url": "https://github.com/wojtek-krysiak",
			"followers_url": "https://api.github.com/users/wojtek-krysiak/followers",
			"following_url": "https://api.github.com/users/wojtek-krysiak/following{/other_user}",
			"gists_url": "https://api.github.com/users/wojtek-krysiak/gists{/gist_id}",
			"starred_url": "https://api.github.com/users/wojtek-krysiak/starred{/owner}{/repo}",
			"subscriptions_url": "https://api.github.com/users/wojtek-krysiak/subscriptions",
			"organizations_url": "https://api.github.com/users/wojtek-krysiak/orgs",
			"repos_url": "https://api.github.com/users/wojtek-krysiak/repos",
			"events_url": "https://api.github.com/users/wojtek-krysiak/events{/privacy}",
			"received_events_url": "https://api.github.com/users/wojtek-krysiak/received_events",
			"type": "User",
			"site_admin": false
		},
		"labels": [

		],
		"state": "open",
		"locked": false,
		"assignee": null,
		"assignees": [

		],
		"milestone": null,
		"comments": 0,
		"created_at": "2019-11-13T21:01:28Z",
		"updated_at": "2019-11-13T21:01:28Z",
		"closed_at": null,
		"author_association": "CONTRIBUTOR",
		"body": ""
	}
]

Go back to the nocks.js file. The third property are the returned headers of the response. We won’t use them in tests so let’s remove them.

Now, modify the mocking function to take repo as a param.

This is how it looks:

// spec/nocks.js
const nock = require('nock')

const getIssuesResponse = require('./get-issues-response.json')

module.exports.getIssues = ({ repoName }) => {
  return nock('https://api.github.com:443', {"encodedQueryParams":true})
  .get(`/repos/${repoName}/issues`)
  .reply(200, getIssuesResponse);
}

Test with the real API calls

Ok - we are ready. Let’s create our test - but first, without using nock. Go back to client.spec.js,

Let’s describe the getIssues function. In beforeAll filter set up the test subject

We will write 2 tests. First, specifying that there is just one issue. Add async because the function will be asynchronous. Now write the expectation.

Go to the json file - let’s check if the first issue has a title containing `example issue no. 1` text.

Write a second test case. Similar - it will be an async function. Store the response in a variable and test the first item.

This is how the test looks:

describe('#getIssues', function () {
  beforeEach(function () {
    this.client = new Client({ repoName: REPO_NAME })
  })

  it('returns one issue', async function () {
    expect(await this.client.getIssues()).to.have.lengthOf(1)
  })

  it('returns issues with correct title', async function() {
    const issues = await this.client.getIssues()

    expect(issues[0].title).to.equal('example issue no.1')
  })
})

Ok - Go back to the client.js file and implement this method. Create response const. Now use the axios instance we created in the constructor to fetch issues. Put the repoName into the URL. And return data from that response.

This is how it looks:

async getIssues() {
  const response = await this.api.get(`repos/${this.repoName}/issues`)

  return response.data
}

Run tests

And we see that they pass.

Stubbing the HTTP server with nock

But obviously we cannot leave it like that, for at least 2 reasons.

  • One - those tests are taking a super long time and second...
  • when someone adds an issue to the GitHub repo the entire test will fail.

So now - let’s change that by using nock.

First, require it no the top of our spec.js file, and below -  disable all the real API calls. Do this with disableNetConnect() function.

const nock = require('nock')
nock.disableNetConnect()

Rerun the tests again and we see NetConnectNotAllowedError - which says that our tests are trying to access the real API.

Ok - so now let’s intercept the request by the function we previously defined.

Require it and rename to “stubGetIssues” and use it in beforeEach hook:

const { getIssues: stubGetIssues } = require('../spec/nocks')
...
describe('Client', function () {
  ...
  describe('#getIssues', function () {
    beforeEach(function () {
      this.stubResponse = stubGetIssues({ repoName: REPO_NAME })
      ...
    })
    ...
  })
})

It returns the request stub, let’s store because you might want to use it later on.

Pass repo name and save it.

Rerun tests one more time.

Perfect - it runs super fast and everything goes from our recorded responses.

Ok. This was all. Now I encourage you to check out the nock documentation page. It is an amazing tool and has a lot of interesting features.

I hope you liked this episode. If you did, subscribe the channel and see you next time.