20 Jul 2016 · Software Engineering

    Mocking External HTTP Requests in Node Tests with Nock

    11 min read
    Contents

    Introduction

    Testing code that depends on external services and APIs, e.g. the GitHub API, has a variety of challenges. Writing tests that make real HTTP requests to these services may be error-prone due to issues such as network connectivity, API changes, rate limiting and so on. Nock allows us to avoid these challenges by intercepting external HTTP requests and enabling us to return custom responses, making unit testing easier.

    By the end of this article, you will:

    • Learn how to run unit tests against a real HTTP endpoint,
    • Learn how to return different responses to different HTTP endpoints, and
    • Know how to record and playback live HTTP requests to make testing easier.

    Prerequisites

    We will need the latest stable version of Node.js, v4.4.7 at the time of writing.

    Project Setup

    In this section, we will set up a simple application which will serve as a starting point for testing out Nock.

    Let’s start by creating an empty folder where we will store our Node.js project, then navigate into the newly-created folder.

    mkdir nock-test
    cd nock-test

    Once we’re’ inside the folder, we can initialize our Node.js project by running:

    npm init

    This will require a few responses from you. Accepting the defaults is okay, for now.

    After running the above command, you should have a package.json file in your working directory that should look like this, depending on how you answered the prompts.

    {
      "name": "nock-test",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }

    Next, we need to install the nock package and save it to our project’s development dependencies. We will also make use of Mocha as our testing framework, along with the Chai assertion library which helps us specify what we expect to happen in our tests.
    This command installs them all at a go:

    npm install nock mocha chai --save-dev

    We also need to install SuperAgent and save it to our project dependencies. We will use this package to send HTTP requests to the GitHub API.

    npm install superagent --save

    Mocha expects us to store our tests in a directory called test under out project folder

    mkdir test

    We can run the tests using the mocha command installed in our local node_modules folder, as follows:

    ../node_modules/mocha/bin/mocha

    However, it is much easier to save this as the test command under the scripts section in our package.json file.

      "scripts": {
        "test": "./node_modules/mocha/bin/mocha"
      },

    Now, we can run the tests by just running npm test.

    Testing with Nock

    Let’s go on and write our first test. We will follow the test-driven development convention of writing our tests first. We will create a simple script to list a user’s followers on GitHub. This is a good example since it allows us to see the performance of the tests before and after using nock.

    Let’s go ahead and create the first test:

    touch test/index.spec.js

    This is the initial test that we will start with:

    var expect = require('chai').expect;
    var getUserFollowers = require('../index').getUserFollowers;
    
    describe('GET followers', function() {
      it('returns users followers', function(done) {
        // Increase the default timeout for this test
        // If the test takes longer than this, it will fail
        this.timeout(3000);
    
        var username = 'octocat';
    
        getUserFollowers(username, function(err, followers) {
          // It should return an array object
          expect(Array.isArray(followers)).to.equal(true);
          // Ensure that at least one follower is in the array
          expect(followers).to.have.length.above(1);
          // Each of the items in the array should be a string
          followers.forEach(function(follower) {
            expect(follower).to.be.a('string');
          });
          done();
        });
    
      });
    });

    Let’s go over the test we’ve just written:

    We are importing a getUserFollowers function from the index.js file in the root directory of our project.

    This function takes in a GitHub username and a callback function. The callback function is called with an error and an array of followers. We are testing that the array of followers has at least one follower and that it is composed of elements that are all strings.

    In our implementation of the function, the returned array should consist of usernames of users that follow the user we provide to the function.

    Running this test should fail with the following error:

    Error: Cannot find module '../index'

    Writing the GitHub Followers Script

    Since we have the tests set up and we know what to expect, we can proceed and implement the actual script.

    Let’s create the index.js file in our root directory:

    touch index.js

    index.js

    var request = require('superagent');
    
    var getUserFollowers = function(username, callback) {
      request
        .get(https://api.github.com/users/${username}/followers)
        .end(function(err, res) {
          if (!err) {
            var users = res.body.map(function(user) {
              return user.login;
            });
            callback(null, users);
          } else {
            callback('Error Occurred!');
          }
        });
    };
    
    module.exports.getUserFollowers = getUserFollowers;

    Let’s go over our script. We are using the superagent package to make a request to the GitHub API followers endpoint to fetch a user’s followers. Here is a sample response from this request.

    From the sample response, we can see that the response body contains an array of user objects in the following format:

    {
      "login": "octocat",
      "id": 583231,
      "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=3",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    }

    In our script, we are first making a request to the GitHub API and if no error occurs, we then iterate over each of the user objects and extract the login property — username, from each of the user’s followers.

    If an error does occur while trying to fetch the URL, due to issues such as network connectivity, we return a custom response, informing the user that an error has occurred.

    Finally, we export the function so that it can be used elsewhere, for example, in the test script.

    This test fulfills the requirements of our tests and when we run the tests again with npm test, we should see that they pass.

    However, one thing to note is how long the tests take. Here are the results for the passing test:

    > nock-test@1.0.0 test /Users/kevin/code/nock-test
    > ./node_modules/mocha/bin/mocha
    
    
      GET followers
        ✓ returns user followers (2150ms)
    
    
      1 passing (2s)

    It takes 2 seconds to run this simple test. This happens because we are making the actual request to the GitHub API every time we run the test. That’s simply too long. If we had network connectivity issues, such as running the tests while offline, the test would fail, because of the factors that are out of our control, which would make our tests undependable.

    This is where nock comes in.

    Introducing Nock

    At the very basic level, Nock is a library that helps us provide custom responses to HTTP requests made in our application and is handy for use during testing.

    With Nock, we can specify the response we want to return when a certain URL is accessed by our application. This way, our tests will not be affected by issues outside of our control.

    Let’s introduce nock in our index.spec.js file.

    index.spec.js

    var expect = require('chai').expect;
    var nock = require('nock');
    var getUserFollowers = require('../index').getUserFollowers;
    
    describe('GET followers', function() {
      beforeEach(function() {
        var followersResponse = [{
          "login": "octocat",
          "id": 583231,
          "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=3",
          "gravatar_id": "",
          "url": "https://api.github.com/users/octocat",
          "html_url": "https://github.com/octocat",
          "followers_url": "https://api.github.com/users/octocat/followers",
          "following_url": "https://api.github.com/users/octocat/following{/other_user}",
          "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
          "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
          "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
          "organizations_url": "https://api.github.com/users/octocat/orgs",
          "repos_url": "https://api.github.com/users/octocat/repos",
          "events_url": "https://api.github.com/users/octocat/events{/privacy}",
          "received_events_url": "https://api.github.com/users/octocat/received_events",
          "type": "User",
          "site_admin": false
        }, {
          "login": "nanocat",
          "id": 583233,
          "avatar_url": "https://avatars.githubusercontent.com/u/583233?v=3",
          "gravatar_id": "",
          "url": "https://api.github.com/users/nanocat",
          "html_url": "https://github.com/nanocat",
          "followers_url": "https://api.github.com/users/nanocat/followers",
          "following_url": "https://api.github.com/users/nanocat/following{/other_user}",
          "gists_url": "https://api.github.com/users/nanocat/gists{/gist_id}",
          "starred_url": "https://api.github.com/users/nanocat/starred{/owner}{/repo}",
          "subscriptions_url": "https://api.github.com/users/nanocat/subscriptions",
          "organizations_url": "https://api.github.com/users/nanocat/orgs",
          "repos_url": "https://api.github.com/users/nanocat/repos",
          "events_url": "https://api.github.com/users/nanocat/events{/privacy}",
          "received_events_url": "https://api.github.com/users/nanocat/received_events",
          "type": "User",
          "site_admin": false
        }];
    
        // Mock the TMDB configuration request response
        nock('https://api.github.com')
          .get('/users/octocat/followers')
          .reply(200, followersResponse);
      });
    
      it('returns users followers', function(done) {
    
        var username = 'octocat';
    
        getUserFollowers(username, function(err, followers) {
          // It should return an array object
          expect(Array.isArray(followers)).to.equal(true);
          // Ensure that at least one follower is in the array
          expect(followers).to.have.length.above(1);
          // Each of the items in the array should be a string
          followers.forEach(function(follower) {
            expect(follower).to.be.a('string');
          });
          done();
        });
    
      });
    });

    Before we delve into what is going on in the test, let’s run the tests once again to see if our tests are performing any better.

    > nock-test@1.0.0 test /Users/kevin/code/nock-test
    > ./node_modules/mocha/bin/mocha
    
    
      GET followers
      ✓ returns user followers
    
    
      1 passing (30ms)

    This time, we can see that our tests run in 30ms. Your speed may differ but it should be around the same, give or take.

    Analyzing the Nock Approach

    Now, it’s time to go over the version of the test that uses Nock and the benefits that it gives us when testing.

    In the beforeEach function, which is run before each of the tests are run, we set our mock response to replace the actual response we expect from the GitHub API.

    To set the response which will be returned when we call the GitHub API, we’ll do this:

      nock('https://api.github.com')
        .get('/users/octocat/followers')
        .reply(200, followersResponse);
      });

    This means that when we call the https://api.github.com/users/octocat/followers route, the response that should be returned has a 200 status code and returns the content in the followersResponse variable as the response body.

    This is also the reason why our tests run so fast. Nock is aware that when our script tries to access the same URL call of that route in our actual script, it should intercept that HTTP request and return our mock response instead of accessing the actual GitHub service.

    This is an advantage for us because we want to test the functionality of our code, and not the accessibility of the services it might depend on in order to work.

    Summary

    We have worked through creating a simple script in Node.js that uses an external service and tested it, both with and without Nock.

    At first, we implemented the tests by accessing the actual service. Then, we switched to relying on Nock to prevent access to the real service and mimic the conditions under which our application would be run.

    Nock is a robust library can do much more than this, so it’s a good idea to take a closer look at it and explore more of its functionalities. If you have any questions or comments about this tutorial, feel free to leave them in the comments section below.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Kevin is a Full Stack Web Developer specializing in Python, JavaScript and React. He occasionally blogs about his experiences. Find him online under the username @kevgathuku.