MongoError: pool is draining, new operations prohibited when using MongoMemoryServer in integration test - node.js

I'm using MongoMemoryServer to write an integration test. I have two integration test files. When I run the IT tests I see the following. I don't understand why. I'm using jestjs test framework.
I'm seeing the following error when I have two IT test files
MongoError: pool is draining, new operations prohibited
37 | for (const key in collections) {
38 | const collection = collections[key];
> 39 | await collection.deleteMany();
| ^
40 | }
41 | };
Here is my setup
//db-handler.js
const mongoose = require("mongoose");
const { MongoMemoryServer } = require("mongodb-memory-server");
const mongod = new MongoMemoryServer();
module.exports.connect = async () => {
const uri = await mongod.getConnectionString();
const mongooseOpts = {
useNewUrlParser: true,
autoReconnect: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 1000,
};
await mongoose.connect(uri, mongooseOpts);
};
module.exports.closeDatabase = async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongod.stop();
};
module.exports.clearDatabase = async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
const collection = collections[key];
await collection.deleteMany();
}
};
All my IT tests setup looks like this
//example.it.test.js
const supertest = require("supertest");
const dbHandler = require("./db-handler");
const app = require("../../src/app");
const request = supertest(app);
const SomeModel = require("../Some");
beforeAll(async () => await dbHandler.connect());
afterEach(async () => await dbHandler.clearDatabase());
afterAll(async () => await dbHandler.closeDatabase());
describe("Some Test Block", () => {
it("Test One", async (done) => {
await SomeModel.create({a: "a", b "b"});
const response = await request.get("/endPointToTest");
expect(response.status).toBe(200);
done();
});
When I have just a single IT test files, everything works fine. When I introduce a a new IT test file similar setup up as example.it.test.js, then the new test fails. The example error message above.
What am I missing? Does my setup need to change when I have multiple IT test files?
UPDATE ONE:
My package.json file looks like
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --runInBand ./tests"
},
"devDependencies": {
"jest": "^25.2.7",
"mongodb-memory-server": "^6.5.2",
"nodemon": "^2.0.2",
"supertest": "^4.0.2"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
]
}
}

I think the error message can be somewhat misleading for some cases.
It seems like the issue is related to "pool size being too small" or "jest runs tests in parallel", so the pool eventually ran out of usable pool.
However, if you look at what happens when mongo destroys the pool, you'll get the idea.
/**
* Destroy pool
* #method
*/
Pool.prototype.destroy = function(force, callback) {
...
// Set state to draining
stateTransition(this, DRAINING);
...
If you try to close the connection, the pool changes its internal state into 'draining' which causes the problem.
So what the error message is telling us is that there's still unfinished writing operation(s) going on after pool.destroy.
For my case, that was an active event listener callback runs every x seconds which does write to mongo.
I changed my test code to call event callback function directly instead of being called every x seconds.
That resolved my issue.

By default Jest runs tests in parallel with a “a worker pool of child processes that run tests” (Jest CLI docs). As per the Jest documentation.
Each test file is making a new connection to the mogodb server which is causing the error.
You could run the Jest test sequentially to avoid this issue.
The --runInBand or -i option of the Jest CLI allows you to run tests sequentially (in non-parallel mode).
This error (pool is draining, new operations prohibited) might occur if you are using the official mongodb library for node js or mongoose. Please specifying pool size while establishing mongodb connection.
module.exports.connect = async () => {
const uri = await mongod.getConnectionString();
const mongooseOpts = {
useNewUrlParser: true,
autoReconnect: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 1000,
poolSize: 10,
};
await mongoose.connect(uri, mongooseOpts);
};

I did the same way and faced into the same error.
This was resolved by removing by schema name in test only like I did below in some.test.js
** Rest all setup remains same **
//example.it.test.js
const dbHandler = require("./db-handler");
const SomeModel = require("../Some");
const SomeOtherModel = require("../SomeOther");
beforeAll(async () => await dbHandler.connect());
afterEach(async () => {
await SomeModel.remove({});
await SomeOtherModel.remove({});
});
afterAll(async () => await dbHandler.closeDatabase());
describe("Some Test Block", () => {
it("Test One", async (done) => {
await SomeModel.create({a: "a", SomeOther: 'SomeOther Data' });
const data = await SomeModel.getAll();
expect(data.length).toBe(1);
done();
});
});

For every It test case, it connects to the mongoose, and if any error occurred, mongoose does not disconnect.
Solution
You can put the it test case in the try catch block, but if possible use 2 point
Try to handle the error in the controllers and service, and so on
files if possible.
And whenever you write test case, there should be no error comes from the development code.
Don't write nested describe in a single describe, max 2 nested
describe we should use.
One temporary solution, I skip the all describe expect outmost where mongoose was connected.
One more temporary solution, disconnect the mongod and then see the status, then reconnect.

Related

Codeceptjs on Google Cloud Function goto of undefined

I'm trying to automate a web activity using CodeceptJS (with Puppeteer) running in a Google Cloud Function.
My index.js is:
const Container = require('codeceptjs').container;
const Codecept = require('codeceptjs').codecept;
const event = require('codeceptjs').event;
const path = require('path');
module.exports.basicTest = async (req, res) => {
let message = '';
// helpers config
let config = {
tests: './*_test.js',
output: './output',
helpers: {
Puppeteer: {
url: 'https://github.com', // base url
show: true,
disableScreenshots: true, // don't store screenshots on failure
windowSize: '1200x1000', // set window size dimensions
waitForAction: 1000, // increase timeout for clicking
waitForNavigation: [ 'domcontentloaded', 'networkidle0' ], // wait for document to load
chrome: {
args: ['--no-sandbox'] // IMPORTANT! Browser can't be run without this!
}
}
},
include: {
I: './steps_file.js'
},
bootstrap: null,
mocha: {},
name: 'basic_test',
// Once a tests are finished - send back result via HTTP
teardown: (done) => {
res.send(`Finished\n${message}`);
}
};
// pass more verbose output
let opts = {
debug: true,
steps: true
};
// a simple reporter, let's collect all passed and failed tests
event.dispatcher.on(event.test.passed, (test) => {
message += `- Test "${test.title}" passed 😎`;
});
event.dispatcher.on(event.test.failed, (test) => {
message += `- Test "${test.title}" failed 😭`;
});
// create runner
let codecept = new Codecept(config, opts);
// codecept.init(testRoot)
codecept.initGlobals(__dirname);
// create helpers, support files, mocha
Container.create(config, opts);
try {
// initialize listeners
codecept.bootstrap();
// load tests
codecept.loadTests('*_test.js');
// run tests
codecept.run();
} catch (err) {
printError(err)
process.exitCode = 1
} finally {
await codecept.teardown()
}
}
and my simple test is:
Feature('Basic Automation');
Scenario('Basic Test', async ({ I }) => {
// ===== Login =====
pause()
I.amOnPage('https://cotps.com');
I.see('Built for developers', 'h1');
});
If I run it using npx codeceptjs run --steps it works but if I run it using node -e 'require("./index").basicTest() I get the error:
Cannot read property 'goto' of undefined. I also get the error if I deploy it to GCP and run it. I've looked through the docs for both Codecept and Puppeteer but found nothing and the only examples online are for previous versions of the libraries.

Sometimes Jest integration tests fails and sometimes don't when I run all the suites

I've written some integration test using Jest, Supertest, Moongose. I run all test isolated (test.only) and they work but sometimes don't. Often this happens when I run the entire test suite. I give you an example: This test creates a new registry in a MongoDB collection and then other test use this new registry to perform another operations:
beforeAll(async () => {
await mongoose.connect(config.mongoose.url, config.mongoose.options);
});
afterAll(async () => {
await mongoose.disconnect();
await new Promise(resolve => setTimeout(() => resolve(), 500));
});
let credentials = [];
let fechaHora = [];
// generate a new Id registry
// generate credentials
// generate date and hour
beforeEach(async () => {
rooms.insertRoomsToFile(rooms.getNewIdRoom() + '|');
_idRoom = rooms.getIdRoom();
credentials = await rooms.generateCredentialsBE(_idOrganization, basicToken);
fechaHora = rooms.generateRoomDateAndHour();
});
test(`endpoint ${BASE_URL}${registerMeetingRoute} : 200 OK (Happy Path)`, async () => {
generatedIdRoom = _idRoom;
const data = {
idOrganization: 1,
idRoom: generatedIdRoom,
date: fechaHora[0],
hour: fechaHora[1],
attendes: [
{
email: "john.doe#example.com.mx",
id: 1,
firstName: "John",
lastName: "Doe",
userAttende: "10000000-0000-0000-0000-000000000000",
rol: 1,
telephone: "5555555555"
},
{
email: "tom.taylor#example.com.mx",
id: 2,
firstName: "Tom",
lastName: "Taylor",
userAttende: "20000000-0000-0000-0000-000000000000",
rol: 2,
telephone: "5555555556"
}
]
};
const encryptedData = await rooms.encrypt(data);
idAccess = encryptedData.idAccess;
await request(app)
.post(`${BASE_URL}${registerMeetingRoute}`)
.set('content-type', 'application/json')
.set('authorization', 'Bearer ' + credentials[2])
.set('x-accessId', idAccess)
.send(encryptedData)
.expect(200);
rooms.saveLog(JSON.stringify(encryptedData), `endpoint ${BASE_URL}${registerMeetingRoute} : 200 OK (Happy Path)`);
});
This works fine, the problem is sometimes don't. I've tried many answers here and read blogs about this topic but I can't solve it. I tried:
Increase testTimeout property in jest.config.js
Open and close MongoDb connection per test
To use mongodb-memory-server
--runInBand option
Thanks in advance :)
Jest will execute different test files in parallel and in a different order from run to run.
You can change this behaviour and write your own custom test sequencer.
Jest will execute all tests within a single file sequentially.
However, I've also encountered issues with the complete file run vs. running a single test, in case a plug-in a router with WebHashHistory into the wrapper mount. The problem does not occur with MemoryHistory:
const router = createRouter({
history: createMemoryHistory(),
routes: myRoutes,
})
Vue 3.2, Vue-router 4.0, vue-test-utils v2.0, options API, SFC, Jest v27.0

TypeError: Slackbot is not a constructor

I need your help because it's the first time that I develop a Slack bot and I don't understand why this message appear:
const botSlack = new Slackbot ({
^
TypeError: Slackbot is not a constructor
Here my code : slack.js
const {Slackbot} = require('#slack/bolt');
const botSlack = new Slackbot({
token : process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
(async () => {
await botSlack.start(process.env.PORT || 3000);
})();
Part of my package.json:
"scripts": {
"dev": "nodemon slack.js",
},
"dependencies": {
"#slack/bolt": "^3.11.0",
},
In the bolt documentation (https://api.slack.com/tutorials/hello-world-bolt) and others, it's the same and it's run.
Please someone can explain to me why ?
The documentation has this code - App instead of Slackbot. You can rename the local variable if you want, but the import statement requires the exact name:
const { App } = require('#slack/bolt');
const botSlack = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET
});
(async () => {
await botSlack.start(process.env.PORT || 3000);
})();
if you do want to rename the value, you can use
const { App: Slackbot } = require('#slack/bolt');
It is more common to use import statements for this, you can still alias it using import { App as Stackbot} from '#slack/bolt', see MDN - import

Knex pool full on migration

I'm trying to get started with knex.js and I can't get migrations to work. Knex works fine for my API calls. Here's my setup:
knexfile.js
const env = process.env;
module.exports = {
client: 'mysql',
connection: {
host: env.DB_HOST,
database: env.DB_NAME,
user: env.DB_USER,
password: env.DB_PASSWORD,
port: env.PORT
},
pool: {
min: 0,
max: 50
},
migrations: {
directory: './db/migrations',
tableName: 'knex_migrations'
},
seeds: {
directory: './db/seeds'
}
};
knex.js
const config = require('../knexfile.js');
module.exports = require('knex')(config);
events.js
const express = require('express');
const router = express.Router();
const knex = require('../../db/knex.js');
// GET api/events
router.get('/', (req, res) => {
knex('events')
.then(events => { res.send(events) }
.catch(err => { console.log(err); })
});
module.exports = router;
and then I have a file in the migrations folder with:
exports.up = function(knex) {
return knex.schema.createTable('users', function (t) {
t.increments('id').primary()
t.string('username').notNullable()
t.string('password').notNullable()
t.timestamps(false, true)
}).then(() => { console.log('created users table') })
.catch((err) => { throw err} )
.finally(() => { knex.destroy() })
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('users')
};
When I run knex migrate:latest I get TimeoutError: Knex: Timeout acquiring a connection. The pool is probably full. Are you missing a .transacting(trx) call?
I know similar questions have been asked before, but I can't seem to find any that shed light on my particular situation. I've tried adding a knex.destroy() to the end of my GET request but that doesn't seem to help (it just makes the connection unusable if I add other request handlers below).
I did try checking the knex.client.pool in a finally clause at the end of the GET request. numUsed was 0, numFree was 1, numPendingAcquires and numPendingCreates were both 0. I do find it odd that numFree was only 1 given that my knexfile specifies max 50. Any advice greatly appreciated.
Following #technogeek1995's comment, the answer turned out to be adding require('dotenv').config({path: '../.env'}); to knexfile.js (in retrospect, this part seems obvious), and running the cli from the same directory. Hope this helps someone else.

nodejs/mocha/chai as promise : variables used in expected async function initialized outside

I am brand new to mocha/chai and I spent 2 days trying to solve the following issue without any success (please note that the code below is just to present the concept, it is not the real one).
I have got a JS file called "api.js" in which some variables such as SERVER_URL are initialized at the top of the file through dotenv framework.
api.js :
const SERVER_URL = process.env.SERVER_URL;
async function startAPI () {
return new Promise ( (resolve, reject) => {
console.log(`${SERVER_URL}`);
resolve();
});
exports = {startAPI};
Now I have got "test.js" file in which :
test.js:
require('../api');
it('the test', async () => {
return await expect(api.startAPI()).to.be.fulfilled;
});
The problem is that SERVER_URL is undefined during the test and I cannot modify the api.js (as I am not the owner), just the test.js.
How can I run the test with the SERVER_URL variable set correctly (to process.env.SERVER_URL value from api.js) ?
Is there a solution without any refactoring ?
And if not what is the best solution ?
Experts, thanks in advance for your precious help
A way to improve testability is to use process.env.SERVER_URL instead of SERVER_URL where possible - or getServerUrl():
const getServerUrl = () => process.env.SERVER_URL;
This way process.env.SERVER_URL can be mocked at any point.
An alternative is to import module after process.env.SERVER_URL was mocked. This should involve decaching if there's more than one test that uses this module, because it won't be re-evaluated otherwise.
const decache = require('decache');
...
let originalServerUrl;
beforeEach(() => {
originalServerUrl = process.env.SERVER_URL;
});
beforeEach(() => {
process.env.SERVER_URL = originalServerUrl;
});
it('the test', async () => {
decache('../api');
process.env.SERVER_URL = '...';
const api = require('../api');
await expect(api.startAPI()).to.be.fulfilled;
});
If it's expected that there's no SERVER_URL in tests, it can be just discarded after it was mocked:
The easiest way would be just to set these variables when you run your test from CLI:
e.g. in npm scripts:
"scripts": {
"test": "SERVER_URL='http://example.com' mocha"
}
or directly from terminal:
$ SERVER_URL='http://example.com' npm test
But better solution would be mock environment variables in your tests with little refactoring. And need proxyquire to be installed. And actually async/await is not needed here.
const proxyquire = require('proxyquire').noPreserveCache() // noPreserveCache is important to always have refreshed script with new process.env.SERVER_URL in each test
const MOCKED_SERVER_URL = 'http://example.com'
describe('example', () => {
let initialServerUrl
let api
beforeEach(() => {
initialServerUrl= process.env
})
afterEach(() => {
process.env = initialServerUrl
})
it('fulfilled', () => {
process.env.USE_OTHER_CODE_PATH = MOCKED_SERVER_URL
api = proxyquire('../api', {})
return expect(api.startAPI()).to.be.fulfilled
})
it('rejected', () => {
process.env.USE_OTHER_CODE_PATH = ''
api = proxyquire('../api', {})
return expect(api.startAPI()).to.be.rejected
})
})
You can set .env variables with mocha using the following line:
env SERVER_URL=htt://api.yourserver.com/ mocha test
This way mocha knows what to expect from your process.env.SERVER_URL

Resources