How to close a database connection after all Mocha tests are finished – Mocha Root Hook Plugins in TypeScript
May 3, 2025 by Andrew Dawes

Today, I learned how to run arbitrary code after all Mocha tests have finished.
I needed to do this because I was writing integration tests for a class that performs CRUD operations on a MySQL database – and I noticed something odd: After printing pass/fail results from each of my tests, the mocha
CLI command would indefinitely “hang”.
It did not take long for me to ascertain that the mocha
process was refusing to exit because the database connection was still open.
However, when I tried destroying the database connection after the first test, the second test would throw an error because it was unable to reconnect to the database.
I realized I could leave the database connection open until all tests in this file were completed and then close the connection at the end of the file.
But, I knew this would not scale well, because I am planning on creating additional classes and unit tests that will also interact with the database.
In an ideal world, the code responsible for cleaning up my database connection should wait to execute until after all my tests were complete.
So, I started looking for ways to hook into Mocha lifecycle events to run arbitrary code.
I found this documentation on creating “Root Hook Plugins” to hook into Mocha’s lifecycle events.
Implementing a Mocha root hook plugin was far easier than I thought!
- Create a
.ts
file - From this
.ts
file, export amochaHooks
object that defines properties such asbeforeEach
,afterEach
,beforeAll
, andafterAll
- Each of these properties is named for the lifecycle event to hook into, and the value of each property should be an array of functions you wish to execute during that lifecycle event
- Compile this
.ts
file before running your tests - Alter your
npm
test script to pass the--require
flag tomocha
. The value of this flag should be the location to which your.ts
file will be transpiled.
I set up a .ts
file (destroy.ts
) that exported a mochaHooks
object with an afterAll
property containing an array with a single item – which was a function that closed my database connection (db.destroy()
).
Below are some code snippets showing how I did this!
My tests:
// src/classes/EventProcessor.spec.ts
import sinon from 'sinon';
import { expect, use } from 'chai';
import sinonChai from 'sinon-chai';
import EventProcessor from './EventProcessor.js';
import { db } from '../connections/database.js';
use(sinonChai);
describe('EventProcessor', () => {
it('processes events with a connection 1x', async () => {
const producer = {
sendMessage: sinon.spy(),
};
const consumer = {
addOnMessageHandler: sinon.spy(),
};
const eventProcessor = new EventProcessor({
db,
producer,
consumer,
});
await eventProcessor.processEvent({
value: Buffer.from(
JSON.stringify({
eventType: 'RequestedAccountList',
})
),
});
expect(producer.sendMessage).to.have.been.calledWith({
key: 'myKey',
value: JSON.stringify({
eventType: 'AcknowledgedAccountList',
data: {
request: {
eventType: 'RequestedAccountList',
},
payload: [
{
lock: {
versionId: 0,
proofOfInclusionBTreeSerialized:
'{"t":3,"root":{"isLeaf":true,"keys":[2],"children":[]}}',
},
object: {
id: 1,
recordStatus: 'ACTIVE',
platformAccountId: 'qwe456',
platformAPIKey: '645rty',
},
},
{
lock: {
versionId: 0,
proofOfInclusionBTreeSerialized:
'{"t":3,"root":{"isLeaf":true,"keys":[2],"children":[]}}',
},
object: {
id: 2,
recordStatus: 'DELETED',
platformAccountId: 'uyt321',
platformAPIKey: 'bnm123',
},
},
],
},
}),
});
});
it('processes events with a connection 2x', async () => {
const producer = {
sendMessage: sinon.spy(),
};
const consumer = {
addOnMessageHandler: sinon.spy(),
};
const eventProcessor = new EventProcessor({
db,
producer,
consumer,
});
await eventProcessor.processEvent({
value: Buffer.from(
JSON.stringify({
eventType: 'RequestedAccountList',
})
),
});
expect(producer.sendMessage).to.have.been.calledWith({
key: 'myKey',
value: JSON.stringify({
eventType: 'AcknowledgedAccountList',
data: {
request: {
eventType: 'RequestedAccountList',
},
payload: [
{
lock: {
versionId: 0,
proofOfInclusionBTreeSerialized:
'{"t":3,"root":{"isLeaf":true,"keys":[2],"children":[]}}',
},
object: {
id: 1,
recordStatus: 'ACTIVE',
platformAccountId: 'qwe456',
platformAPIKey: '645rty',
},
},
{
lock: {
versionId: 0,
proofOfInclusionBTreeSerialized:
'{"t":3,"root":{"isLeaf":true,"keys":[2],"children":[]}}',
},
object: {
id: 2,
recordStatus: 'DELETED',
platformAccountId: 'uyt321',
platformAPIKey: 'bnm123',
},
},
],
},
}),
});
});
});
My database connection:
// src/connections/database.ts
import { Database } from '../interfaces/Database.js'; // this is the Database interface we defined earlier
import { createPool } from 'mysql2'; // do not use 'mysql2/promises'!
import { Kysely, MysqlDialect } from 'kysely';
import { env } from 'process';
const dialect = new MysqlDialect({
pool: createPool({
database: env.DB_NAME,
host: env.DB_HOSTNAME,
user: env.DB_USER,
password: env.DB_PASSWORD,
port: 3306,
connectionLimit: 10,
jsonStrings: true,
}),
});
// Database interface is passed to Kysely's constructor, and from now on, Kysely
// knows your database structure.
// Dialect is passed to Kysely's constructor, and from now on, Kysely knows how
// to communicate with your database.
export const db = new Kysely<Database>({
dialect,
});
My custom Mocha Root Hook Plugin:
// src/scripts/destroy.ts
import { db } from '../connections/database.js';
export const mochaHooks = {
afterAll: [
async function () {
await db.destroy();
},
],
};
My .tsconfig.json settings:
// .tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"strict": true,
"outDir": "dist",
"declaration": true,
"moduleDetection": "force",
"sourceMap": false,
"skipLibCheck": true
},
"include": [
"src/**/*"
],
}
My .mocharc.json settings:
// .mocharc.json
{
"spec": "dist/**/*.spec.js",
"recursive": true
}
My package.json settings:
// package.json
{
"name": "frosttide-broker-account",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"build": "tsc",
"test": "mocha --require ./dist/scripts/destroy.js",
"test:dev": "npm run build && npm run test",
"migrate:make": "cp ./dev/templates/migration.ts ./src/migrations/$(date -Iseconds).ts",
"migrate": "node ./dist/scripts/migrate.js",
"migrate:dev": "rm -rf ./dist/migrations/* && npm run build && npm run migrate",
"seed": "node ./dist/scripts/seed.js",
"seed:dev": "npm run build && npm run seed",
"serve": "node ./dist/index.js",
"dev": "nodemon --watch './src/*' -e ts,js --exec 'npm run build; npm run migrate; npm run serve'",
"prod": "npm run migrate && npm run serve"
},
"devDependencies": {
"@types/chai": "^5.0.1",
"@types/mocha": "^10.0.10",
"@types/node": "^20.17.19",
"@types/sinon": "^17.0.4",
"@types/sinon-chai": "^4.0.0",
"@types/ws": "^8.5.14",
"chai": "^5.2.0",
"mocha": "^11.1.0",
"nodemon": "^2.0.22",
"sinon": "^19.0.2",
"sinon-chai": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.7.3"
},
"dependencies": {
"@umerx/btreejs": "^2.0.0",
"@umerx/kafkajs-client": "^4.0.1",
"kysely": "^0.28.1",
"lmdb": "^3.2.6",
"mysql2": "^3.14.0",
"zod": "^3.24.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/umerx-github/frosttide-broker-account.git"
},
"author": "Missie Dawes",
"license": "ISC",
"bugs": {
"url": "https://github.com/umerx-github/frosttide-broker-account/issues"
},
"homepage": "https://github.com/umerx-github/frosttide-broker-account#readme",
"description": ""
}