'How can I wrap a Mocha or Jest test suite in a Node.js AsyncLocalStorage.run?
I have a test suite for a Node.js+express.js application that requires database access using Sequelize. My previous experience was in Python+Django. In Django, there is a nice TestCase class that wraps each test in a transaction; the transaction is rolled back automatically at the end of each test. This is a good idea to ensure data-wise isolation among tests.
I want to replicate the same effect using Mocha or Jest. I get a feeling that there should be a way using Node.js (v16)'s AsyncLocalStorage, but I just can't get it to work.
My guess at the gist of it is something like this:
- instantiate an AsyncLocalStorage in a module, and export it.
- in my test file, import the AsyncLocalStorage instance;
_ call Mocha's
describe(); - inside the callback for
describe, create a Sequelize transaction; - inside the callback for the Sequelize transaction:
- store the transaction in the AsyncLocalStorage instance
- call Mocha's
beforehook; then, inside the callback for the before hook:- create a Sequelize transaction checkpoint
- insert some fixture rows into the database using that checkpoint
- call Mocha's
afterbook; then, inside the callback for the after hook:- rollback the checkpoint
- call Mocha's
ita couple of times to define each test; inside each test:- call some application functions that I want to test; each of these functions will:
- import the AsyncLocalStorage instance and try to find a transaction in it
- if a transaction is found in AsyncLocalStorage, create another checkpoint on it and use it
- otherwise start a new transaction
- call some application functions that I want to test; each of these functions will:
Doing so would allow the application functions to see the fixture data that I inserted in Mocha's before hook. And then the checkpoint rollback in Mocha's after hook would just clean everything up. I wouldn't have to write my own SQL DELETE queries, for example, and clean up would always be correct. I wouldn't have to worry about one test suite accidentally leaving data that contaminates the next test suite (which would cause a lot of head scratching).
I tried two different ways, but none of them worked (the tests wouldn't actually get run).
Attempt 1:
import { QueryTypes, Transaction } from 'sequelize';
import { applicationFunction1, applicationFunction2 } from '../src/blabla';
import { sequelize, transactionStorage } from '../src/infrastructure/orm';
import { insertFixtureSQL } from './fixture'
const map = new Map<string, Transaction>();
describe('My super awesome test suite', async function() {
// eslint-disable-next-line consistent-this,babel/no-invalid-this
const mocha = this;
// `transactionStorage` is an AsyncLocalStorage instance
// start an AsyncLocalStorage run by giving it the map;
// other async functions down the chain should be able to retrieve the same map;
await transactionStorage.run(map, async () => {
// `sequelize` is a `Sequelize` instance
await sequelize.transaction(async (transaction) => {
// put the transaction in the map
map.set('transaction', transaction);
mocha.beforeEach(async () => {
// Create a checkpoint
const checkpoint = await sequelize.transaction({ transaction });
// Replace the transaction with the checkpoint (which itself is a sequelize Transaction instance)
map.set('transaction', checkpoint);
// Insert fixture data using the checkpoint;
// `insertFixtureSQL` is a string that contains an SQL INSERT statement.
await sequelize.query(insertFixtureSQL, { type: QueryTypes.INSERT, raw: true, transaction: checkpoint });
});
mocha.afterEach(async () => {
const checkpoint = map.get('transaction') as Transaction;
// If I just rollback the checkpoint, everything shall be cleaned up
await checkpoint.rollback();
});
it('My awesome test 1', async () => {
// inside `applicationFunction1()`, it will try to find the transaction from the
// `transactionStorage`;
// if found, it will create another checkpoint on it and use it;
// otherwise, it will create a new Sequelize transaction.
await applicationFunction1();
});
it('My awesome test 2', async () => {
// inside `applicationFunction2()`, it will try to find the transaction from the
// `transactionStorage`;
// if found, it will create another checkpoint on it and use it;
// otherwise, it will create a new Sequelize transaction.
await applicationFunction2();
});
});
});
});
Attempt 2:
import { QueryTypes, Transaction } from 'sequelize';
import { applicationFunction1, applicationFunction2 } from '../src/blabla';
import { sequelize, transactionStorage } from '../src/infrastructure/orm';
import { insertFixtureSQL } from './fixture'
const map = new Map<string, Transaction>();
// `transactionStorage` is an AsyncLocalStorage instance
// start an AsyncLocalStorage run by giving it the map;
// other async functions down the chain should be able to retrieve the same map;
transactionStorage.run(map, async () => {
// `sequelize` is a `Sequelize` instance
await sequelize.transaction(async (transaction) => {
// put the transaction in the map
map.set('transaction', transaction);
describe('My super awesome test suite', async function() {
this.beforeEach(async () => {
// Create a checkpoint
const checkpoint = await sequelize.transaction({ transaction });
// Replace the transaction with the checkpoint (which itself is a sequelize Transaction instance)
map.set('transaction', checkpoint);
// Insert fixture data using the checkpoint;
// `insertFixtureSQL` is a string that contains an SQL INSERT statement.
await sequelize.query(insertFixtureSQL, { type: QueryTypes.INSERT, raw: true, transaction: checkpoint });
});
this.afterEach(async () => {
const checkpoint = map.get('transaction') as Transaction;
// If I just rollback the checkpoint, everything shall be cleaned up
await checkpoint.rollback();
});
it('My awesome test 1', async () => {
// inside `applicationFunction1()`, it will try to find the transaction from the
// `transactionStorage`;
// if found, it will create another checkpoint on it and use it;
// otherwise, it will create a new Sequelize transaction.
await applicationFunction1();
});
it('My awesome test 2', async () => {
// inside `applicationFunction2()`, it will try to find the transaction from the
// `transactionStorage`;
// if found, it will create another checkpoint on it and use it;
// otherwise, it will create a new Sequelize transaction.
await applicationFunction2();
});
});
});
});
The difference between the two attempts is the order of the wrapping:
- Attempt 1:
- Mocha's
describe() transactionStorage.run()sequelize.transaction()- Mocha's
beforeEach/afterEach/it
- Mocha's
- Attempt 2:
transactionStorage.run()sequelize.transaction()- Mocha's
describe() - Mocha's
beforeEach/afterEach/it
What happens when I try to run my test suite:
0 passing (1ms)
info: Executing (55fc411e-2a51-4095-9279-aec3942dc818): START TRANSACTION;
info: Executing (55fc411e-2a51-4095-9279-aec3942dc818): COMMIT;
Which means it didn’t actually find any test to run.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
