In this tutorial we will cover basic strategy of unit testing a service in Nest.js + TypeORM setup. We don’t create an environment for test purpose, but instead, we use pg-mem
adapter to run a local in-memory Postgres simulator instance. In this way the db is always fresh out-of-box and it’s also very fast to run through all the tests.
Setup
When we initialized the Nest.js project, it should already ship the project with some Jest infra installed. To recap some knowledge from previous articles:
- In https://jczhang.com/2022/07/19/startup-mvp-recipes-3-linting-eslint-prettier-supports/, we added some linting support for Jest
- In https://jczhang.com/2022/07/20/startup-mvp-recipes-5-1-a-simple-resource-generated-by-nest-cli-then-configured-part-1/, we generated the code for a simple CRUD on an entity and the barebone of the unit test (the
spec.ts
file) is also generated.
To run local jest testing with our env vars and config, we will add following commands to package.json
// package.json
{
"test:local": "NODE_ENV=local jest",
"test:local:t": "NODE_ENV=local jest -t",
}
// jest config section in package.json
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/$1"
},
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node",
"testPathIgnorePatterns": [
"/node_modules/",
]
}
Install pg-mem
npm install pg-mem --include=dev
Setup pg-mem connection (TypeORM 0.2)
// setupConnection.ts
import { DataType, newDb } from 'pg-mem';
import { Connection } from 'typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { v4 } from 'uuid';
export const setupConnection = async (entities: any[]) => {
const db = newDb({
autoCreateForeignKeyIndices: true,
});
db.public.registerFunction({
implementation: () => 'test',
name: 'current_database',
});
db.registerExtension('uuid-ossp', (schema) => {
schema.registerFunction({
name: 'uuid_generate_v4',
returns: DataType.uuid,
implementation: v4,
impure: true,
});
});
const connection: Connection = await db.adapters.createTypeormConnection({
type: 'postgres',
entities,
namingStrategy: new SnakeNamingStrategy(),
});
await connection.synchronize();
return connection;
};
The example unit test code
The key idea is that we create the testing module with its dependencies (so it doesn’t include every module from app module). In the module we need to override the repository that we provide and inject to the crud service, replacing it with the repository we obtained from pg-mem
connection.
To run the test, use npm run test:local:t 'SmsTemplatesService'
describe('SmsTemplatesService', () => {
let service: SmsTemplatesService;
let connection: Connection;
let oneSmsTemplateId: number;
const createSmsTemplateInput: CreateSmsTemplateInput = {
name: 'Test SMS Template Name',
content: 'Test Content',
cohort: 'newsletter',
};
beforeAll(async () => {
connection = await setupConnection([SmsTemplate]);
const module = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
entities: [SmsTemplate],
synchronize: true,
keepConnectionAlive: true,
namingStrategy: new SnakeNamingStrategy(),
}),
TypeOrmModule.forFeature([SmsTemplate]),
],
providers: [SmsTemplatesService],
})
.overrideProvider(Repository)
.useValue(connection.getRepository(SmsTemplate))
.compile();
service = module.get<SmsTemplatesService>(SmsTemplatesService);
});
afterAll(async () => {
connection.close();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should create one', async () => {
const result = await service.createOneBySellerId(
766,
createSmsTemplateInput,
);
oneSmsTemplateId = result.id;
ObjectTyped.keys(createSmsTemplateInput).forEach((key) => {
expect(result[key]).toEqual(createSmsTemplateInput[key]);
});
expect(typeof result.id).toEqual('number');
});
it('should findAllBySellerId', async () => {
const results = await service.findAllBySellerId(766);
ObjectTyped.keys(createSmsTemplateInput).forEach((key) => {
expect(results[0][key]).toEqual(createSmsTemplateInput[key]);
});
});
it('should findOneBySellerId', async () => {
const result = await service.findOneBySellerId(766, oneSmsTemplateId);
ObjectTyped.keys(createSmsTemplateInput).forEach((key) => {
expect(result[key]).toEqual(createSmsTemplateInput[key]);
});
});
it('should update one', async () => {
const sellerId = 766;
const updateSmsTemplateInput: UpdateSmsTemplateInput = {
id: oneSmsTemplateId,
name: 'Test SMS Template Name: Updated Name',
content: 'Test Content',
cohort: 'newsletter',
};
const result = await service.updateOneBySellerId(
sellerId,
updateSmsTemplateInput,
);
ObjectTyped.keys(updateSmsTemplateInput).forEach((key) => {
expect(result[key]).toEqual(updateSmsTemplateInput[key]);
});
});
it('should remove one', async () => {
const result = await service.removeOneBySellerId(766, oneSmsTemplateId);
ObjectTyped.keys(result.returning).forEach((key) => {
expect(result[key]).toEqual(createSmsTemplateInput[key]);
});
expect(result.affectedRows).toEqual(1);
});
});
Pingback: [Startup MVP recipes #10] Nest.js Unit Testing: Mocking service and the universal mock - James Zhang
Pingback: [Startup MVP recipes #11] Nest.js Run Unit Tests with Github Actions - James Zhang