Mocha beforeEach vs before execution

I ran into a problem recently that I can't explain. I have alot of code in these tests so I'm going to do my best to capture the idea here

I have tests that look like:

describe('main page', function() {
  beforeEach(function(done){
    addUserToMongoDb(done);   // #1
  });

  afterEach(function(done) {
    removeUserFromMongoDb(done);
  });

  context('login', function() {
     it('should log the user in', function() {
       logUserIn(user_email);  // #2 - This line requires the user from the beforeEach
     });
  });

  context('preferences', function() {
    before(function(done) {    //#3
       logUserInBeforeTest(user_email);
     });

    it('should show the preferences', function() {
       doCheckPreferences(); // #4
    });
  });
});

The problem is, the beforeEach by #1 runs fine. I can see it happening on the DB and the tests in #2 pass.

However, the tests in the preferences context at #4 fail because it cant find the user to log them in at #3.

It seems that the context before is executed before the describe beforeEach, which causes them to fail. If I move logUserIn into the it block it works fine.

What could cause this?


Solution 1:

While this answer just shows the documentation notes again, and had some comments to try to help show the difference, the answer below by @tomasz-wszelaki should be refered to.

Mocha's test runner explains this functionality the best in the Hooks section of the Mocha Test Runner.

From the Hooks section:

describe('hooks', function() {

    before(function() {
        // runs before all tests in this file regardless where this line is defined.
    });

    after(function() {
        // runs after all tests in this file
    });

    beforeEach(function() {
        // runs before each test in this block
    });

    afterEach(function() {
        // runs after each test in this block
    });

    // test cases
});

You can nest these routines within other describe blocks which can also have before/beforeEach routines.

Solution 2:

I found a similar issue. The documentation is misleading because "before this block" means (to me at least) "before this describe section". Meanwhile it means "before any describe section". Check this example:

describe('outer describe', function () {
    beforeEach(function () {
        console.log('outer describe - beforeEach');
    });

    describe('inner describe 1', function () {
        before(function () {
            console.log('inner describe 1 - before');
        });

    describe('inner describe 2', function () {
        beforeEach(function () {
            console.log('inner describe 2 - beforeEach');
        });
 });

 // output will be:
 // inner describe 1 - before
 // outer describe - beforeEach
 // inner describe 2 - beforeEach

It seems it doesn't matter where in your hierarchy you put the before - it will run before any describe and not before its containing describe.

Solution 3:

The reason of confusion is lies in the documentation of mocha. You can find in mocha:

Tests can appear before, after, or interspersed with your hooks. Hooks will run in the order they are defined, as appropriate; all before() hooks run (once), then any beforeEach() hooks, tests, any afterEach() hooks, and finally after() hooks (once).

Discussed hooks before and beforeEach are executed right before all or each it respectively - there are no way to execute it before describe section.

Here you can find answer of the #1 contributor to the mocha's master branch to the idea add something like beforeDescribe hook.

I think you should look at the --delay mocha option.

Solution 4:

The key thing is to have mocha.opts file with the line pointing on ./test/bootstrap.js, where you apply before, beforeAll, after, afterAll hooks.

Execute all tests:
 - npm test

Execute a single test:
- NODE_ENV=test node --inspect ./node_modules/.bin/_mocha --opts test/mocha.opts test/test/service/unSubscriber.test.js 

node --inspect flag for debugging


/package.json

{
  "name": "app",
  "version": "0.0.1",
  "engines": {
    "node": "11.9.0",
    "npm": "6.5.0"
  },
  "scripts": {
    "test": "NODE_ENV=test node --inspect ./node_modules/.bin/_mocha --opts test/mocha.opts test/**/**/**/*.js"
  },
  "private": true,
  "dependencies": {
    "express": "3.21.2",
    "mongoose": "^4.5.10",
    ...
  },
  "devDependencies": {
    "chai": "^4.2.0",
    "faker": "^4.1.0",
    "mocha": "^6.0.0"
  }
}

/test/mocha.opts

--recursive
--timeout 30000
--reporter spec
--file ./test/bootstrap.js

/test/bootstrap.js

const mongoose = require('mongoose');
const config = require('./../service/config').getConfig();
mongoose.Promise = global.Promise;

before((done) => {
  (async () => {
    const connection = await mongoose.connect(config.mongo_url, { useMongoClient: true });
    await connection.db.dropDatabase();
  })().then(() => {
    require('../server');
    done();
  });
});

after((done) => {
  process.kill(process.pid, 'SIGTERM');
  done();
});

/server.js

const http = require('http');
const app = require('./app');
const config = require('./service/config');
const port = process.env.PORT || 4000;

const server = http.createServer(app);

server.listen(port, () => {
  console.log(`===== Server running:${config.getEnv()}=====`);
});

process.on('SIGTERM', () => {
  console.log('===== Server closed =====');
  process.exit(0);
});

/test/service/unSubscriber.test.js

const faker = require('faker');

const ContactOptOutRepository = require('../../repository/contactOptOut');
const UnSubscriber = require('../../service/unSubscriber');
const expect = require('chai').expect;

const contactOptOutRepository = new ContactOptOutRepository();
const unSubscriber = new UnSubscriber();

const emails = [
  faker.internet.email(),
  faker.internet.email(),
  faker.internet.email(),
  faker.internet.email(),
  faker.internet.email(),
];

describe('UnSubscriber', () => {
  it('should filter out unsubscribed emails', () => {
    return (async () => {
      await contactOptOutRepository.newUnSubscription(emails[0], faker.lorem.word());
      await contactOptOutRepository.newUnSubscription(emails[1], faker.lorem.word());
      await contactOptOutRepository.newUnSubscription(emails[2], faker.lorem.word());

      return await unSubscriber.filterUnsubscribed(emails);
    })()
      .then(filtered => {
        expect(filtered.length).to.be.equal(2);
      });
  });
});