Testing can be a real drag, but let’s face it — it’s a necessary evil if we would like to make sure our code is functioning properly. But what if I told you, you could make your tests run faster and be more efficient? Well, that’s exactly what I did. I was able to achieve this by using jest as my test runner and doing integration tests for my GraphQL resolvers.
I specified the file paths for my global setup and teardown functions, and setupFile in jest config file by adding these lines to my jest.config.js file:
module.exports = {
...
globalSetup: '<rootDir>/tests/util/config/globalSetup.ts',
globalTeardown: '<rootDir>/tests/util/config/globalTeardown.ts',
setupFilesAfterEnv: ['<rootDir>/tests/util/config/setupFile.ts']
...
};
First, I used the package mongodb-memory-server to create an in-memory MongoDB instance for my tests. This way, I didn’t have to wait for my tests to spin up a real MongoDB instance and I saved resources by not having to run a real instance during my tests. It was like having a personal assistant that only worked during tests — a Test Assistant if you will.
- globalSetup.ts file:
import { MongoMemoryServer } from 'mongodb-memory-server';
import { MongoClient } from 'mongodb';
declare global {
interface NodeJS {
__MONGOINSTANCE: MongoMemoryServer;
}
}
export = async function globalSetup(): Promise<void> {
const { RUN_IN_MEMORY, MEMORY_DB } = getEnvVars('RUN_IN_MEMORY', 'MEMORY_DB');
if (MEMORY) {
// Config to decided if a mongodb-memory-server instance should be used
// it's needed in global space, because we don't want to create a new instance every test-suite
const instance: MongoMemoryServer = await MongoMemoryServer.create({
instance: { dbName: MEMORY_DB },
});
const uri = instance.getUri();
globalThis.__MONGOINSTANCE = instance;
// override the config with the new uri and DB name
process.env.MONGO_URL = uri.slice(0, uri.lastIndexOf('/'));
process.env.MONGO_DB = MEMORY_DB;
} else {
const { MONGO_URL, MONGO_DB } = getEnvVars('MONGO_URL', 'MONGO_DB');
// Use a real mongodb instance
// Clean before all the tests starts
await MongoClient.connect(MONGO_URL, { useUnifiedTopology: true }).then(
async (client: MongoClient) => {
await client.db(MONGO_DB).dropDatabase();
await client.close();
},
);
}
};
This is a global setup file that is used to configure the environment for running tests. The file uses the mongodb-memory-server package to create an in-memory MongoDB instance for tests. It checks an environment variable RUN_IN_MEMORY to decide if a mongodb-memory-server instance should be used. The instance is stored in a global variable so that it can be accessed throughout the tests. If the RUN_IN_MEMORY variable is not set, a real MongoDB instance is used and the database is cleaned before all tests start. The file also overrides the MONGO_URL and MONGO_DB environment variables with the appropriate values for the in-memory or real MongoDB instance.
- globalTeardown.ts
import { MongoMemoryServer } from 'mongodb-memory-server';
import { getEnvVars } from '../getEnvVariables';
const { RUN_IN_MEMORY } = getEnvVars('RUN_IN_MEMORY');
export = async function globalTeardown() {
if (MEMORY) {
const instance: MongoMemoryServer = globalThis.__MONGOINSTANCE;
await instance.stop({ force: true });
}
};
This globalTeardown is a global teardown function that stops the in-memory MongoDB instance created in the global setup, if the RUN_IN_MEMORY environment variable is set to true.
Please note that getEnvVars is a custom helper function that retrieves environment variables from the process.env object, and throws an error if any of them are missing.
But wait, there’s more! I also leveraged the power of InversifyJS, a powerful dependency injection library, to manage and access my seed data across different test files. This approach allowed me to easily retrieve an instance of my seed module from the InversifyJS container, simplifying the process of seeding data and making it available throughout my tests.
Alright, let’s stop yapping and get down to business. Let’s jump into the code:
- seedModule.ts
import { inject, injectable } from 'inversify';
import { MongoClient } from 'mongodb';
@injectable()
export class SeedModule {
private testData;
private client: MongoClient;
constructor(@inject('MongoClient') client: MongoClient) {
this.client = client;
}
async seedData() {
/* 2 default users */
const user1 = this.client
.db(process.env.MONGO_DB)
.collection(collectionName)
.insertOne()
const user2 = /* user2 */
/* other test data */
/* ... */
this.testData = {
users: [user1, user2],
};
}
getTestData() {
return this.testData;
}
}
- inversify.config.ts
import { Container } from 'inversify';
import { SeedModule } from './seedModule';
const container = new Container();
container
.bind<SeedModule>('SeedModule')
.to(SeedModule)
.inSingletonScope();
export { container };
This file creates an instance of the InversifyJS container and binds the SeedModule to it. The bound module is set to a singleton scope, meaning that only one instance of the module is created and shared throughout the application. It exports the container object so it can be imported and used in other parts of the application.
import 'reflect-metadata';
import { MongoClient } from 'mongodb';
import { SeedModule } from '../mocks/seed/seedModule';
import { container } from '../mocks/seed/inversify.config';
import { getEnvVars } from '../getEnvVariables';
const { MONGO_URL, MONGO_DB } = getEnvVars('MONGO_URL', 'MONGO_DB');
let mongoDBClient: MongoClient;
beforeAll(async () => {
mongoDBClient = await new MongoClient(MONGO_URL, {
useUnifiedTopology: true,
});
container.bind<MongoClient>('MongoClient').toConstantValue(mongoDBClient);
await mongoDBClient.connect();
await container.get<SeedModule>('SeedModule').seedData();
});
afterAll(async () => {
await mongoDBClient.db(MONGO_DB).dropDatabase();
await mongoDBClient.close();
});
setupFile is used to seed data into a MongoDB instance before all in a test suite are run. It uses InversifyJS for dependency injection and the MongoClient to connect to the MongoDB instance. The file also uses the beforeAll and afterAll Jest hooks to seed data before all tests and clear the database after all tests are finished in the same test suite.
Finally, and to wrap it all up, this is how we can access our seeded values in our test files:
import { container } from '../../util/mocks/seed/inversify.config';
import { SeedModule } from '../../util/mocks/seed/seedModule';
describe('AccountAdmin', () => {
let users: AccountModel[];
beforeAll(async () => {
const testData = await container.get<SeedModule>('SeedModule').getTestData();
users = testData.users;
/*...*/
});
afterAll(async () => {
/*...*/
});
it('...', async () => {
});
});
One of the benefits of using the InversifyJS library for dependency injection, is to manage and access my seed data across different test files. This allowed me to easily get an instance of my seed module from the InversifyJS container and use it to seed my data in a more efficient and organized way. This can be especially helpful when working with multiple test files and needing to access the same seed data across all of them. InversifyJS also allows for more flexibility in managing dependencies, making it easier to switch between different implementations or mock data during testing. Overall, the use of InversifyJS in conjunction with an in-memory MongoDB instance greatly improves the efficiency and organization of the testing process.
Final words :
Combining the use of an in-memory MongoDB instance and InversifyJS dependency injection, I was able to create a more efficient and organized testing process. My tests ran faster and I didn’t have to worry about managing and accessing my seed data. I felt like a boss, or even better, a Test Boss.
In conclusion, if you want to speed up your tests and make them more efficient, give in-memory MongoDB and InversifyJS a try. It may just make your life a little easier, or at least give you an edge in test efficiency.
Testing leads to failure, and failure leads to understanding. — Burt Rutan