Jest not terminating after tests complete successfully
Asked Answered
S

3

18

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;
Saez answered 17/5, 2017 at 23:17 Comment(0)
S
16

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;
}
Saez answered 18/5, 2017 at 19:9 Comment(3)
I would like to add as well that there is no need the unlock() the modules here. It was just an issue of the database connection still being open and preventing jest from exiting with 0.Saez
for me doing await mongoose.disconnect(); was enough. I am not running server.close();. Also, in case anyone else (like me) has more than one connection (opened with createConnection() rather than connect()) you just need one mongoose.disconnect(); rather than connection1.disconnect() & conneciton2.disconnect()Inhaul
where are you getting the object 'server' in line 8 from?Thigh
S
3

My setup has supertest and mongodb-memory-server.

None of the techniques advised here solved the problem for me.

mongodb-memory-server connection ends when it runs alone. But when i combine it with supertest jest doesnt terminate. And jest doesnt show up any open handle error either.

Jest terminated with success only after I did run with --forceExit command.

https://jestjs.io/docs/cli#--forceexit

Stumble answered 7/7, 2022 at 8:46 Comment(2)
Okay. But where do I add --forceExit in my code?Jurisprudent
@Jurisprudent It's not in the code, it's a command line flag. Run jest with jest --forceExit. That said, it's not advised. Better to clean up resources gracefully.Asta
A
-3

this is what it worked for me:

afterAll(async () => {
    try {
      // Connection to Mongo killed.
      await mongoose.disconnect();
    } catch (error) {
      console.log(`
        You did something wrong dummy!
        ${error}
      `);
      throw error;
    }
});
Argonaut answered 13/12, 2019 at 12:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.