Search code examples
node.jstestingasynchronousmocha.jsnock

How do I make Nock and Mocha play well together?


I am trying to use nock to intercept/mock some HTTP traffic in my application for testing purposes. Our app authenticates to another one of our sites, and I need nock to imitate an HTTP 200 (with JSON data) and an HTTP 401 (with no data) to test behaviors when the user is or isn't logged in there (respectively).

I have two tests which both work correctly when run alone, but if I run the entire test suite, one of them always fails. I realize that nock is shared state because it modifies how node.js itself handles network traffic and I assume that's the cause of the race condition, but I can't be the only person who's ever used two different nock interceptors for the same request in two different tests, so I know I'm missing something.

Can anyone help me figure out why these tests are stepping on each other?

My question is related to How to retest same URL using Mocha and Nock? but I did the things suggested there and they didn't help.

My test files (which, again, both work fine if called individually, but fail when run as part of the same test pass) look like this:

import { expect } from 'chai';
import nock from 'nock';

import * as actionTypes from '../../src/constants/action-types';
import * as panoptes from '../../src/services/panoptes';

import { user } from '../modules/users/test-data';

const stagingHost = 'https://my-staging-server.org';

describe('Panoptes', () => {
  afterEach(function (done) {
    nock.cleanAll();
    nock.disableNetConnect();
    done();
  });

  beforeEach(function (done) {
    nock.cleanAll();
    nock.disableNetConnect();
    done();
  });

  describe('with a valid user', function (done) {
    let lastAction = null;

    const scope = nock(stagingHost)
      .get(/^\/oauth\/authorize/)
      .reply(302, '', {
        'location': 'https://localhost:3000',
        'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
        'X-Frame-Options': 'SAMEORIGIN',
        'X-XSS-Protection': '1; mode=block',
      });

    scope
      .get(/^\/api\/me/)
      .reply(200, {
        users: [user],
      });

    panoptes.checkLoginUser((action) => { lastAction = action; }).then(() => {
      nock.removeInterceptor(scope);
      done();
    });

    it('should know when somebody is logged in', function () {
      expect(lastAction).to.not.be.null;
      expect(lastAction.type).to.equal(actionTypes.SET_LOGIN_USER);
      expect(lastAction.user).to.not.be.null;
      expect(lastAction.user.id).to.equal(user.id);
      expect(lastAction.user.login).to.equal(user.login);
    });
  });
});

and

import { expect } from 'chai';
import nock from 'nock';

import * as actionTypes from '../../src/constants/action-types';
import * as panoptes from '../../src/services/panoptes';

const stagingHost = 'https://my-staging-server.org';

describe('Panoptes', () => {
  afterEach(function (done) {
    nock.cleanAll();
    nock.disableNetConnect();
    done();
  });

  beforeEach(function (done) {
    nock.cleanAll();
    nock.disableNetConnect();
    done();
  });

  describe('with no user', function (done) {
    let lastAction = null;

    const scope = nock(stagingHost)
      .get(/^\/oauth\/authorize/)
      .reply(302, '', {
        'Cache-Control': 'no-cache',
        'location': 'https://my-staging-server.org/users/sign_in',
        'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
        'X-Frame-Options': 'SAMEORIGIN',
        'X-XSS-Protection': '1; mode=block',
      });

    scope
      .get(/^\/api\/me/)
      .reply(401);

    panoptes.checkLoginUser((action) => { lastAction = action; }).then(() => {
      nock.removeInterceptor(scope);
      done();
    });

    it('should know that nobody is logged in', function () {
      expect(lastAction).to.not.be.null;
      expect(lastAction.type).to.equal(actionTypes.SET_LOGIN_USER);
      expect(lastAction.user).to.be.null;
    });
  });
});

Solution

  • I think the problem is not in nock, but with the order of your mocha hook's execution order:

    Take this example:

    describe('Panoptes', () => {
    
      afterEach(function () {
        console.log('ORDER: after each');
      });
    
      beforeEach(function () {
        console.log('ORDER: before each');
      });
    
      describe('with a valid user', function () {
    
        console.log('ORDER: with a valid user');
    
        it('should know when somebody is logged in', function () {
          console.log('ORDER: should know when somebody is logged in');
        });
    
      });
    
      describe('with no user', function () {
    
        console.log('ORDER: with no user');
    
        it('should know that nobody is logged in', function () {
          console.log('ORDER: should know that nobody is logged in');
        });
    
      });
    
    });
    

    When we run it we get the following order on output:

    ORDER: with a valid user
    ORDER: with no user
    ORDER: before each
    ORDER: should know when somebody is logged in
    ORDER: after each
    ORDER: before each
    ORDER: should know that nobody is logged in
    ORDER: after each
    

    afterEach/beforeEach runs before and after each it, however the describe body gets evaluated before those hooks are called. You should wrap each of your nocks inside a before. (Also describe does not use a done argument)

    Something like this should work:

    describe('with no user', function () {
    
      before(function() {
        const scope = nock(stagingHost)
          .get(/^\/oauth\/authorize/)
          .reply(302, '', {
            'Cache-Control': 'no-cache',
            'location': 'https://my-staging-server.org/users/sign_in',
            'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
            'X-Frame-Options': 'SAMEORIGIN',
            'X-XSS-Protection': '1; mode=block',
          });
    
        scope
          .get(/^\/api\/me/)
          .reply(401);
      });
    
    
      it('should know that nobody is logged in', function (done) {
        panoptes.checkLoginUser((action) => {
          expect(action).to.not.be.null;
          expect(action.type).to.equal(actionTypes.SET_LOGIN_USER);
          expect(action.user).to.be.null;
          done();
        });
      });
    
    });