Ensure All Nock Interceptors Are Used

Published Nov 29, 2017Last updated Dec 17, 2017

nock is a Node.js testing library that mocks HTTP calls, allowing one to test code that makes HTTP requests without actually making any network calls. However, HTTP mocks are applied at a global level, which could cause unexpected and undesired behaviour.

Summary

Ensure no tests leave behind unused nock interceptors to prevent one test from affecting another test's state, and be proactive by running the following after each test:

// Example for mocha; translate as needed for your favourite test framework
afterEach(function() {
  ...
  if(!nock.isDone()) {
    this.test.error(new Error('Not all nock interceptors were used!'));
    nock.cleanAll();
  }
  ...
});

A Simple Nock Example

Consider the following (examples using mocha and chai):

'use strict';
const request = require('superagent');

const express = require('express');
const app = express();

app.get('/', function(req, res) {
  request
    .get('https://www.example.com')
    .then(response => res.send(response.text));
});

app.listen(3000);
module.exports = app;

To test this, I can use nock like this:

'use strict';
const assert = require('chai').assert;
const nock = require('nock');
const request = require('supertest');
const uniqueValue = 'da64daaf-182b-4af6-a4af-09727bf8d5aa';

const app = require('../../src/server');

describe('server', function() {
  it('GET / once', function() {
    nock('https://www.example.com')
      .get('/')
      .reply(200, uniqueValue);

    return request(app)
      .get('/')
      .then(res => assert.equal(res.text, uniqueValue));
  });
});

For every nock() call, we create an interceptor for a specific request. When that request is made, the interceptor responds based on how it was set up. In this case, it will return uniqueValue, which I used to assert the output. This keeps tests from making real network calls and allows us to test the logic inside our code.

Nock Interceptors Are One-Use

One caveat is that once a nock interceptor is used, it gets removed from the list of interceptors and no longer intercepts that particular request. This is important to keep in mind for the example below.

For example, suppose I am interested in testing the behaviour based on making the same call five times (perhaps there's a cache I want to test for). Because the code in server.js will be calling https://www.example.com five times, I have to set up five interceptors:

'use strict';
const assert = require('chai').assert;
const nock = require('nock');
const request = require('supertest');
const uniqueValue = 'da64daaf-182b-4af6-a4af-09727bf8d5aa';

const app = require('../../src/server');

describe('server', function() {
  it('GET / once', function() {
    nock('https://www.example.com')
      .get('/')
      .reply(200, uniqueValue);

    return request(app)
      .get('/')
      .then(res => assert.equal(res.text, uniqueValue));
  });

  it('GET / five times', function() {
    nock('https://www.example.com')
      .get('/')
      .times(5)   // sets up 5 interceptors
      .reply(200, uniqueValue);

    return request(app)
      .get('/')
      .then(res => assert.equal(res.text, uniqueValue));
  });
});

Notice the bug? I created five nock interceptors, but only used up one! However, since we're not actually validating the use of all five interceptors, both tests will still pass.

Unused Interceptors Causes Unexpected Errors

Suppose we now want to provide a nice error if https://www.example.com is down. I can update the server.js file like this:

'use strict';
const request = require('superagent');

const express = require('express');
const app = express();

app.get('/', function(req, res) {
  request
    .get('https://www.example.com')
    .then(response => res.send(response.text))
    // add a catch here and return a custom message
    .catch(response => res.status(503).send('Please try again later.'));
});

app.listen(3000);
module.exports = app;

We should also add a new test for this new functionality:

'use strict';
const assert = require('chai').assert;
const nock = require('nock');
const request = require('supertest');
const uniqueValue = 'da64daaf-182b-4af6-a4af-09727bf8d5aa';

const app = require('../../src/server');

describe('server', function() {
  it('GET / once', function() {
    nock('https://www.example.com')
      .get('/')
      .reply(200, uniqueValue);

    return request(app)
      .get('/')
      .then(res => assert.equal(res.text, uniqueValue));
  });

  it('GET / five times', function() {
    nock('https://www.example.com')
      .get('/')
      .times(5)   // sets up 5 interceptors
      .reply(200, uniqueValue);

    return request(app)
      .get('/')
      .then(res => assert.equal(res.text, uniqueValue));
  });

  it('GET / fails gracefully if example.com is down', function() {
      nock('https://www.example.com')
        .get('/')
        .reply(500);

      return request(app)
        .get('/')
        .then(res => assert.equal(res.status, 503));
    });
});

This is where the four remaining interceptors start causing problems:

  server
    √ GET / once (46ms)
    √ GET / five times
    1) GET / fails gracefully if example.com is down


  2 passing (76ms)
  1 failing

  1) server GET / fails gracefully if example.com is down:
     AssertionError: expected 200 to equal 503
      at test/server.js:38:29

Because there were four interceptors left that returned a status of 200, the new test used up one of those interceptors first, instead of the one we set up inside the test. However, that error is not very obvious at first glance.

Ensure No Interceptors Remain After Each Test

The fix here is to call the route five times:

return Promise.all(
  Array(5).fill(0).map(i =>
    request(app)
      .get('/')
      .then(res => assert.equal(res.text, uniqueValue));
  )
);

However, to be even more proactive, we can leverage nock's .isDone() and .cleanAll():

'use strict';
const assert = require('chai').assert;
const nock = require('nock');
const request = require('supertest');
const uniqueValue = 'da64daaf-182b-4af6-a4af-09727bf8d5aa';

const app = require('../../src/server');

describe('server', function() {
  afterEach(function() {
    if(!nock.isDone()) {
      this.test.error(new Error('Not all nock interceptors were used!'));
      nock.cleanAll();
    }
  })
  it('GET / once', function() {
    nock('https://www.example.com')
      .get('/')
      .reply(200, uniqueValue);

    return request(app)
      .get('/')
      .then(res => assert.equal(res.text, uniqueValue));
  });

  it('GET / five times', function() {
    nock('https://www.example.com')
      .get('/')
      .times(5)   // sets up 5 interceptors
      .reply(200, uniqueValue);

    return request(app)
      .get('/')
      .then(res => assert.equal(res.text, uniqueValue));
  });

  it('GET / fails gracefully if example.com is down', function() {
      nock('https://www.example.com')
        .get('/')
        .reply(500);

      return request(app)
        .get('/')
        .then(res => assert.equal(res.status, 503));
    });
});

This ensures that tests do not leave any unused nock interceptors behind and simultaneously prevents one test's unused interceptors from affecting other tests:

  server
    √ GET / once (47ms)
    √ GET / five times
    1) GET / five times
    √ GET / fails gracefully if example.com is down


  3 passing (78ms)
  1 failing

  1) server GET / five times:
     Error: Not all nock interceptors were used!
      at Context.<anonymous> (test/server.js:12:23)

Source Code

Feel free to play around with the source code at node-nock-example.

(Reposted)

Discover and read more posts from Mark Tse
get started