pgPromise how to kill all db connections on Jest unit test completion - node.js

In my Jest unit test file that uses pgPromise library I am getting this error:
Jest did not exit one second after the test run has completed.
I guess this should be because of the code below that I have in my app.ts
import {db} from './shared/db';
...
let sco: any;
db.connect().then(function (obj: any) {
sco = obj;
obj.client.query('LISTEN "table_update"');
obj.client.on('notification', function (data: any) {
var result = JSON.parse(data.payload);
log.info(result);
globalEmitter.emit(
eventTypes.Entity_Data_Changed,
result
}
catch(error) {
log.error({ error }, 'Error happened');
})
.finally(() => {
// release the connection, if it was successful:
if (sco) {
sco.done();
}
});;
I use the same import in my unit test file.
db is coming from
const db = pgp(options);
In my unit test file I have this code at the end
afterAll(async done => {
setImmediate(done); // jest address open handle with --detectOpenHandles
db.$pool.end();
pgp.end();});
I tried pgp.end() to kill all connections as I suspect the db expression in app.ts file does not belong to a connection pool as I read somewhere about it before. But still no luck. any help would be appreciated here.

Related

How can I either wait or prevent jest from running my db connection in Sequelize, so as to stop the error "Cannot log after tests are done..."

Im trying to add unit tests to my node express app. The app uses sequelize with sqlite as its orm/database. I am trying to unit test one of my simplest controllers directly (not even trying to test routes with supertest yet)
I was able to get one basic successful test, the issue I a having is that since my controller imports my sequelize User model, it also imports the instantiation of Sequelize. This results in sequelize trying to connect to the DB while the tests are performed. The test is executed before all the db stuff happens, and hence at the end my result is a lot of "Cannot log after tests are done" because Sequelize is still trying to connect and log things, some from the sequelize module itself and others from my code connecting and sync()ing the db.
If I remove the import and usage of the model in my controller, I no longer have those issues, so clearly importing the model is causing all those calls to initialize sequelize. How can i ask Jest to either wait until all async processes are done, or just prevent sequelize from initializing at all (I dont actually need a db connection, i will test them all with mocks)
So here is what the test looks like
describe('testing user registration', () => {
let mRes: Response<any, Record<string, any>>;
let mNext;
beforeAll(()=>{
mRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
}
mNext = jest.fn(err => err)
jest.spyOn(UserModel, 'create').mockResolvedValue(createdUser)
})
afterAll(()=>{
jest.resetAllMocks();
})
test('with invalid email', async () => {
const result = await registerUser(mReqInvalidEmail, mRes, mNext);
expect(mNext).toBeCalledWith(invalidBodyError)
})
})
Here is what the Controller looks like:
const registerUser = async(req:Request,res:Response,next:NextFunction) : Promise<Promise<Response> | void> => {
const {body} = req
const {email, role} = body;
if(!isValidEmail(email) || !isValidRole(role)){
const error = createError(
400,
'Invalid body, please provide a valid email address a role of oneOf[\"user\",\"super\"]',
{
body: {
email: 'a valid email string',
role: 'string, oneOf [user, super]'
}
}
);
return next(error);
}
const password = generatePassword()
const hash = hashPassword(password)
const user = await User.create({email, role, password:hash}).catch((err: Error) : Error => {
return createError(500, 'woopsie', err)
})
if(user instanceof Error){
return next(user)
}
return res.status(200).json({
email,
role,
password
});
}
The model:
import {sqlzDB} from "../../database/db";
const User = sqlzDB.define('User',
{
...myfields, not including to save space
}, {
options
})
And finally, where sequelize gets initialized (declaring sqlzDB). This is all the code that is running that I need to either wait for it to finish, or just prevent it from getting called at all!
const {Sequelize} = require('sequelize');
export const sqlzDB = new Sequelize({
dialect: 'sqlite',
storage: 'database/db.sqlite'
});
sqlzDB.authenticate()
.then(():void => {
console.log('Connection to database established successfully.');
}).catch((err : Error): void => {
console.log('Unable to connect to the database: ', err);
})
sqlzDB.sync().then(()=>{
console.log('Sequelize Synced')
})
My test passes just fine. Note that for the test i wrote i dont actually need the mocks yet, Since im just trying to get the setup to work correctly.
I have tried suggestions I have seen out here like calling await new Promise(setImmediate); after my test, or also closing the connection with sqlzDB.close() which causes some different issues (it closes the connection but still tries to log)
So i dont know how to approach this at this point! Sorry for th elong question and thanks to whoever took their time to read this!
You can try a trick to avoid this issue: Close the connection in afterAll
afterEach(() => { // reset mock should be in afterEach
jest.resetAllMocks();
});
afterAll(async ()=>{
await sqlzDB.close();
await new Promise(res => setTimeout(res, 500)); // avoid jest open handle error
});

Best way to setup files for jest and mongodb

Hihi.
I'm having a problem where jest it's sometimes failing a couple of tests randomly, most of the time because of this error "mongodb memory server cannot perform operation: a background operation is currently running for collection".
In another post I read something about building differents mongo instance for each block of tests.
What I have so far is a globalsetup file where I start the mongo replica set like this:
// global.ts
import { MongoMemoryReplSet } from "mongodb-memory-server";
const replSet = new MongoMemoryReplSet({
replSet: { storageEngine: "wiredTiger" },
});
module.exports = async () => {
await replSet.waitUntilRunning();
const uri = await replSet.getUri();
process.env.MONGODB_URI = uri;
};
and my db.ts is like this
// db.ts
export const connect = async () => {
mongoose.set("useFindAndModify", false);
const conn = mongoose.connect(
process.env.MONGODB_URI || config.connectionString, connectionSettings
);
When trying to call it from a test file I do something like this
// test.spec.ts
import db from "./db";
beforeAll(async () => {
await db.connect();
});
afterAll(async (done) => {
await db.dropCollections();
await db.disconnect(done);
});
beforeEach(async () => {
await seed();
});
describe('Some test', () => {
it('Should not fail and get the seeders', () => {
// some random tests using the seeds values
})
})
What I read in that post is instead of using globalSetup use a setupFile that will run for every test instead of one globally and then I MIGHT be able to solve the concurrency issue I have with my tests.
So, to conclude, does anyone knows if there is a proper way to configure the mongodb in memory or if I am doing something THAT BAD that is allowing this to happend or if is there any kind of improvement I can do that will prevent "mongodb memory server cannot perform operation: a background operation is currently running for collection" this to happen?

Integration Test with TypeORM

I'm doing an integration test by making requests to my dev server by Supertest. But I have trouble with how to put data into the database. For example, before run GET test, I want to insert data to the database. But I even can't get a connection from TypeORM:
ConnectionNotFoundError: Connection "default" was not found.
If I even get the connection from TypeORM, how I wrap a test within transaction and rollback transaction after finish the test to make sure the integration test doesn't effect to a real database.
Basically, Is there any package that similar with factory_bot of Rails ?
describe("Templates", (): void => {
describe("GET /api/v1/templates/{templateHash}", (): void => {
let template: Template;
beforeEach(
async (): Promise<void> => {
let templateService: TemplateService = new TemplateService();
let connection: Connection = getConnection("default");
template = new Template(Buffer.from(faker.random.words()), faker.random.uuid(), faker.system.fileName());
await templateService.create(connection, template);
},
);
it("should return a template", (done): void => {
request(config.devEnvEndpoint)
.get(`api/v1/templates/${template.templateHash}`)
.set("Accept", "application/json")
.expect(200)
.end((err, response): void => {
console.log(response);
done();
});
});
});
});
It looks like typeorm is not picking up your config. I recommend using the ormconfig.json file and have two database configs - one for your development and one for testing.
By having two databases, you won't need to worry about transactions and can drop the database between tests. I suppose transactions would be faster but it really depends on your usecase (I noticed tests were much slower when making lots of connection.synchronize() calls).
Then you could use an environment variable to determine which connection to instantiate:
// db.ts
import { Connection, createConnection } from 'typeorm'
let connection: Connection | null = null
export async function getConnection() {
if (connection) {
return connection;
}
// DATABASE_CONFIG can be 'development' or 'test'
// and should correspond to the connection names
// in the ormconfig.json file
connection = await createConnection(process.env.DATABASE_CONFIG);
return connection;
}
export async function closeConnection() {
await connection?.close();
}
Then you can set the environment variable and run your tests:
// DATABASE_CONFIG=test yarn test
import { getConnection, closeConnection } from './db'
let connection: Connection;
beforeAll(async () => {
connection = await getConnection();
});
beforeEach(async () => {
// Drop everything and recreate db
await connection.synchronize(true);
});
afterAll(async () => {
await closeConnection();
});
it('should create a connection', () => {
expect(connection).toBeDefined();
})

How to use jest.spyOn function to test a function from another file

I am trying to run test case for testing entry point of my web service jest
I am facing one issue while running the unit test.
Cannot spy the async function, because it is a property not a function.
I am trying to test if the run function is called in the server.js file
The entry file is somewhat like below.
import config from 'config';
export default async function run() {
try {
/*
some code
*/
} catch (err) {
process.exit(1);
}
}
run();
And the test file is like below
import run from '../server';
describe('server-test', () => {
it('run', async () => {
const start = require('../server');
const spy = jest.spyOn(start, run);
await run();
expect(spy).toBeCalled();
});
});
The test should run properly,
but I am getting below error on running this test
Cannot spy the async function run() {
try {
/*
some code.
*/
} catch (err) {
process.exit(1);
}
} property because it is not a function; undefined given instead
I researched for quite a long for this error and ended on the following post
How to spy on a default exported function with Jest?
So, without ditching the default keyword (i.e required for ES 6 lint), the only solution is to use 'default' word in place of 'run'.
Use somewhat like this.
const spy = jest.spyOn(start, 'default');
It should work.

jest doesn't wait beforeAll resolution to start tests

What I test: An express server endpoints
My goal: automate API tests in a single script
What I do: I launch the express server in a NodeJS child process and would like to wait for it to be launched before the test suite is run (frisby.js endpoints testing)
What isn't working as expected: Test suite is launched before Promise resolution
I rely on the wait-on package which server polls and resolves once the resource(s) is/are available.
const awaitServer = async () => {
await waitOn({
resources: [`http://localhost:${PORT}`],
interval: 1000,
}).then(() => {
console.log('Server is running, launching test suite now!');
});
};
This function is used in the startServer function:
const startServer = async () => {
console.log(`Launching server http://localhost:${PORT} ...`);
// npmRunScripts is a thin wrapper around child_process.exec to easily access node_modules/.bin like in package.json scripts
await npmRunScripts(
`cross-env PORT=${PORT} node -r ts-node/register -r dotenv/config src/index.ts dotenv_config_path=.env-tests`
);
await awaitServer();
}
And finally, I use this in something like
describe('Endpoints' () => {
beforeAll(startTestServer);
// describes and tests here ...
});
Anyway, when I launch jest the 'Server is running, launching test suite now!' console.log never shows up and the test suite fails (as the server isn't running already). Why does jest starts testing as awaitServer obviously hasn't resolved yet?
The npmRunScripts function works fine as the test server is up and running a short while after the tests have failed. For this question's sake, here's how npmRunScripts resolves:
// From https://humanwhocodes.com/blog/2016/03/mimicking-npm-script-in-node-js/
const { exec } = require('child_process');
const { delimiter, join } = require('path');
const env = { ...process.env };
const binPath = join(__dirname, '../..', 'node_modules', '.bin');
env.PATH = `${binPath}${delimiter}${env.PATH}`;
/**
* Executes a CLI command with `./node_modules/.bin` in the scope like you
* would use in the `scripts` sections of a `package.json`
* #param cmd The actual command
*/
const npmRunScripts = (cmd, resolveProcess = false) =>
new Promise((resolve, reject) => {
if (typeof cmd !== 'string') {
reject(
new TypeError(
`npmRunScripts Error: cmd is a "${typeof cmd}", "string" expected.`
)
);
return;
}
if (cmd === '') {
reject(
new Error(`npmRunScripts Error: No command provided (cmd is empty).`)
);
return;
}
const subProcess = exec(
cmd,
{ cwd: process.cwd(), env }
);
if (resolveProcess) {
resolve(subProcess);
} else {
const cleanUp = () => {
subProcess.stdout.removeAllListeners();
subProcess.stderr.removeAllListeners();
};
subProcess.stdout.on('data', (data) => {
resolve(data);
cleanUp();
});
subProcess.stderr.on('data', (data) => {
reject(data);
cleanUp();
});
}
});
module.exports = npmRunScripts;
I found the solution. After trying almost anything, I didn't realize jest had a timeout setup which defaults at 5 seconds. So I increased this timeout and the tests now wait for the server promise to resolve.
I simply added jest.setTimeout(3 * 60 * 1000); before the test suite.
In my case, it caused by the flaw of the beforeAll part. Make sure the beforeAll doesn't contain any uncaught exceptions, otherwise it will behaves that the testing started without waiting for beforeAll resolves.
After much digging I found a reason for why my beforeAll didn't seem to be running before my tests. This might be obvious to some, but it wasn't to me.
If you have code in your describe outside an it or other beforeX or afterY, and that code is dependent on any beforeX, you'll run into this problem.
The problem is that code in your describe is run before any beforeX. Therefore, that code won't have access to the dependencies that are resolved in any beforeX.
For example:
describe('Outer describe', () => {
let server;
beforeAll(async () => {
// Set up the server before all tests...
server = await setupServer();
});
describe('Inner describe', () => {
// The below line is run before the above beforeAll, so server doesn't exist here yet!
const queue = server.getQueue(); // Error! server.getQueue is not a function
it('Should use the queue', () => {
queue.getMessage(); // Test fails due to error above
});
});
});
To me this seems unexpected, considering that code is run in the describe callback, so my impression was that that callback would be run after all beforeX outside the current describe.
It also seems this behavior won't be changed any time soon: https://github.com/facebook/jest/issues/4097
In newer versions of jest (at least >1.3.1) you can pass a done function to your beforeAll function and call it after everything is done:
beforeAll(async (done) => {
await myAsyncFunc();
done();
})
it("Some test", async () => {
// Runs after beforeAll
})
More discussions here: https://github.com/facebook/jest/issues/1256

Resources