Jest not terminating after tests complete successfully

rockchalkwushock picture rockchalkwushock · May 18, 2017 · Viewed 9.5k times · Source

I'm working with jest & supertest to test my api endpoints and the tests are passing with no problems; however jest is never exiting with the Done in X amount of time statement that I would normally see. I looked to make sure the --watch flag was not being used and it was not. It seems as if the connection to the server is never ending so jest is left wondering what is next. UserFactory.generate() creates fake user data using the faker library.

I'm at a loss on trying to troubleshoot this. I've followed the recommended strategies on jest's help page with no luck and been digging around in the issue tracker as well but see nothing about similar issues.

This is what I will see on running my test suite:

enter image description here

As you can see the test is ran 7 passes. I'm told that all tests have been run and then morgan shows the POST that has happened. jest never exits so in reality this is failing because it will timeout on any CI sever barring a manual exit.

I have tried using .end() & done() as well as .then() & done() in place of the async/await. They all are returning the same end result and if it were an issue of a promise not resolving jest will error out over an unresolved promise so I am beyond stumped on why this is not terminating as jest normally would.

Has anyone encountered an issue like this before?

user.controller.test.js

import mongoose from 'mongoose';
import request from 'supertest';
import { UserFactory } from '../../__mocks__';
import { User } from '../../modules';
import { config } from '../../utils';
import app from '../../';

const mockRoute = data => request(app).post(`${config.ENDPOINT}/user/sign-up`).send(data);

describe(`POST: /user/sign-up`, () => {
  // remove any user data from db prior to running tests.
  beforeAll(async () => { await User.remove(); });
  test('Returns status 201 on success.', async () => {
    // Returns the response object: res.status === { status }
    const { status }  = await mockRoute(UserFactory.generate());
    expect(status).toEqual(201);
  });
  afterAll(async () => {
    // drop connection to the collection
    const { users } = mongoose.connection.collections;
    await users.drop();
  });
});

user/routes.js

import { Router } from 'express';
import validate from 'express-validation';

import { signUp } from './controller';
import valid from './validation'

const routes = new Router();

/**
 * 1. Define the route: 'user/signup'.
 * 2. Validate the data being provided on the POST
 *    against valid.signUp object.
 * 3. Provide data to signUp controller method for
 *    creating a user in the database.
 */
 routes.post('/user/sign-up', validate(valid.signUp), signUp);

 export default routes;

user/controller.js

import HTTPStatus from 'http-status';
import User from './model';
import { config, filterBody } from '../../utils';

export const signUp = async (req, res, next) => {
  const filteredBody = filterBody(req.body, config.WHITELIST.users.signUp);
  try {
    const user = await User.create(filteredBody);
    return res.status(HTTPStatus.CREATED).json(user.toAuthJSON());
  } catch (e) {
    e.status = HTTPStatus.BAD_REQUEST;
    return next(e);
  }
}

user/model.js

import mongoose, { Schema } from 'mongoose';
import uniqueValidator from 'mongoose-unique-validator';
import { hashSync, compareSync } from 'bcrypt-nodejs';
import jwt from 'jsonwebtoken';

import { config } from '../../utils';

const UserSchema = new Schema({
  email: {
    type: String,
    unique: true,
    required: [true, 'Email is required!'],
    trim: true,
    validate: {
      validator(email) {
        const emailRegex = /^[-a-z0-9%S_+]+(\.[-a-z0-9%S_+]+)*@(?:[a-z0-9-]{1,63}\.){1,125}[a-z]{2,63}$/i;
        return emailRegex.test(email);
      },
      message: '{VALUE} is not a valid email!',
    }
   },
   password: {
      type: String,
      required: [true, 'Password is required!'],
      trim: true,
      minlength: [6, 'Password need to be longer!'],
      validate: {
        validator(password) {
        return password.length >= 6 && password.match(/\d+/g);
        },
      },
  }
}, { timestamps: true })

UserSchema.plugin(uniqueValidator, {
  message: '{VALUE} already taken!',
});

UserSchema.pre('save', function(next) {
  if (this.isModified('password')) {
   this.password = this._hashPassword(this.password);
   return next();
  }
  return next();
});


UserSchema.methods = {
  authenticateUser(password) {
    return compareSync(password, this.password);
  },
  _hashPassword(password) {
    return hashSync(password);
  },
  createToken() {
    return jwt.sign({ _id: this._id }, config.JWT_SECRET);
  },
  toAuthJSON() {
    return {
      _id: this._id,
      token: `JWT ${this.createToken()}`,
    };
  },
  toJSON() {
    return {
      _id: this._id,
      username: this.username,
    };
  },
};

let User;

try {
   User = mongoose.model('User');
} catch (e) {
   User = mongoose.model('User', UserSchema);
}

export default User;

user/validation.js

import Joi from 'joi';

export default {
   signUp: {
    body: {
      email: Joi.string().email().required(),
      password: Joi.string()
        .min(6)
        .regex(/^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)$/)
        .required(),
    },
  },
};

middlewares.js

import bodyParser from 'body-parser';
import compression from 'compression';
import cors from 'cors';
import morgan from 'morgan';
import { userRoutes } from '../modules';

export default app => {
  app.use(compression());
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(cors());
  app.use(morgan('dev'));
  // applying api routes last.
  app.use('/api/v1', [userRoutes]);
}

index.js

import express from 'express';
import {
  database,
  config,
  middlewares,
} from './utils';

// Create instance of Express.
const app = express();
const MODE = process.env.NODE_ENV;

// Apply middlewares to Express.
middlewares(app);
// Connect to the environment determined database.
database(config.MONGO_URI)

app.listen(config.PORT, err => {
  if (err) { return console.error(err); }
  console.log(`App running on port: ${config.PORT} in ${MODE} mode.`);
});

export default app;

Answer

rockchalkwushock picture rockchalkwushock · May 18, 2017

Figured out the issue was that I was not doing enough in the afterAll() hook.

I made a small repo to reproduce the issue and troubleshoot it from there and this was what needed to be done so that jest could exit on successful completion of the test suite:

afterAll(async () => {
try {
  const { todos } = mongoose.connection.collections;
  // Collection is being dropped.
  await todos.drop()
  // Connection to Mongo killed.
  await mongoose.disconnect();
  // Server connection closed.
  await server.close();
} catch (error) {
  console.log(`
    You did something wrong dummy!
    ${error}
  `);
  throw error;
}