Testing Strategies for a NestJS + Mikro-ORM App with Jest
2024-09-20
2024-09-20
When building an application with NestJS and Mikro-ORM in TypeScript, ensuring proper testing is essential to maintain code quality and reliability. In this post, I will cover three main testing strategies for database-related operations, each with its pros and cons.
In this approach, you set up an in-memory SQLite database during tests to simulate persistence without interacting with a real database.
Pros:
Cons:
import { MikroORM } from '@mikro-orm/core';
import { User } from './user.entity'; // example entity
import { SqliteDriver } from '@mikro-orm/sqlite';
describe('User Service - In-Memory DB', () => {
let orm: MikroORM;
beforeAll(async () => {
orm = await MikroORM.init({
entities: [User],
dbName: ':memory:',
type: 'sqlite',
});
const generator = orm.getSchemaGenerator();
await generator.createSchema();
});
afterAll(async () => {
await orm.close(true);
});
it('should persist and retrieve a user entity', async () => {
const userRepo = orm.em.getRepository(User);
const user = userRepo.create({ name: 'John Doe' });
await userRepo.persistAndFlush(user);
const retrievedUser = await userRepo.findOne({ name: 'John Doe' });
expect(retrievedUser).toBeDefined();
expect(retrievedUser.name).toBe('John Doe');
});
});
This setup is relatively straightforward, but keep in mind the limitations regarding database compatibility. Note also this approach is not recommended by the Mikro-ORM creator but in the Mikro-ORM repo it is used anyway for some tests.
Another option is to initialize Mikro-ORM with the same driver you'd use in production but prevent it from connecting to a real database by setting connect: false
. This can be a quick setup, especially when you don't need to run real database queries.
Pros:
Cons:
import { MikroORM } from '@mikro-orm/core';
import { User } from './user.entity';
describe('User Service - No DB Connection', () => {
let orm: MikroORM;
beforeAll(async () => {
orm = await MikroORM.init({
entities: [User],
dbName: 'test-db',
type: 'postgresql', // same as production
connect: false, // prevent real connection
});
});
it('should mock user creation and retrieval', async () => {
const mockUser = { id: 1, name: 'Mock User' };
const userRepo = orm.em.getRepository(User);
jest.spyOn(userRepo, 'persistAndFlush').mockImplementation(async () => mockUser);
jest.spyOn(userRepo, 'findOne').mockResolvedValue(mockUser);
await userRepo.persistAndFlush(mockUser);
const foundUser = await userRepo.findOne({ name: 'Mock User' });
expect(foundUser).toBeDefined();
expect(foundUser.name).toBe('Mock User');
});
});
This approach works well for unit tests where database interaction is mocked. However, the lack of actual persistence may make your tests less reliable.
Mocking everything is an approach where you mock both the repository methods and any related services to simulate the behavior of the database without involving the actual ORM operations. See example an example in the nestjs-realworld-example-app here.
Pros:
Cons:
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { User } from './user.entity';
import { getRepositoryToken } from '@mikro-orm/nestjs';
describe('User Service - Full Mock', () => {
let userService: UserService;
const mockRepository = {
persistAndFlush: jest.fn(),
findOne: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{ provide: getRepositoryToken(User), useValue: mockRepository },
],
}).compile();
userService = module.get<UserService>(UserService);
});
it('should create and return a user', async () => {
const mockUser = { id: 1, name: 'Mock User' };
mockRepository.persistAndFlush.mockResolvedValue(mockUser);
mockRepository.findOne.mockResolvedValue(mockUser);
const createdUser = await userService.create({ name: 'Mock User' });
const foundUser = await userService.findOne({ name: 'Mock User' });
expect(createdUser).toEqual(mockUser);
expect(foundUser).toEqual(mockUser);
});
});
This is particularly useful in unit tests where the focus is on testing business logic rather than database interaction.
Choosing the right testing strategy depends on the scope and type of your tests:
Consider mixing and matching these approaches based on the requirements of your project to balance accuracy, speed, and simplicity.