Testing is an essential part of building reliable and maintainable NestJS applications. When working with database-driven services, it’s crucial to validate not just isolated logic (unit testing) but also full application behavior (integration testing).
This comprehensive guide will walk you through:
- Writing unit tests for NestJS services that access the database using mocks
- Writing integration tests using real database connections
- Using
@nestjs/testing
to test full request-response cycles
Table of Contents
- Why Test Services with DB Access?
- Unit Testing: Isolated Service Logic
- Integration Testing with Real Database
- End-to-End (E2E) Testing with HTTP Requests
- Best Practices
- Conclusion
Why Test Services with DB Access?
Testing services that communicate with a database ensures:
- Correct handling of data
- Consistency between business logic and persistence layer
- Fewer runtime bugs
- Confidence when refactoring
Unit Testing: Isolated Service Logic
Unit tests validate small, isolated pieces of functionality using mocked dependencies instead of real ones.
Mocking the Repository
Create a mock version of your TypeORM repository.
tsCopyEdit// test/mocks/user-repository.mock.ts
export const mockUserRepository = () => ({
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
});
Writing Unit Tests with Jest
tsCopyEdit// user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';
import { mockUserRepository } from '../test/mocks/user-repository.mock';
describe('UserService - Unit', () => {
let service: UserService;
let repo;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: getRepositoryToken(User),
useFactory: mockUserRepository,
},
],
}).compile();
service = module.get(UserService);
repo = module.get(getRepositoryToken(User));
});
it('should find user by email', async () => {
const mockUser = { id: 1, email: '[email protected]' };
repo.findOne.mockResolvedValue(mockUser);
const result = await service.findByEmail('[email protected]');
expect(result).toEqual(mockUser);
});
});
Integration Testing with Real Database
While unit tests mock everything, integration tests use actual database connections, verifying the real behavior of repositories and services.
Setup Using SQLite (In-Memory)
Use an in-memory SQLite DB to avoid modifying production data and ensure fast test runs.
tsCopyEdit// user.service.int-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service';
import { User } from './user.entity';
describe('UserService - Integration', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
dropSchema: true,
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
],
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should create and fetch user from real DB', async () => {
const newUser = await service.createUser({ email: '[email protected]' });
expect(newUser.id).toBeDefined();
const fetchedUser = await service.findByEmail('[email protected]');
expect(fetchedUser.email).toBe('[email protected]');
});
});
End-to-End (E2E) Testing with HTTP Requests
E2E tests use @nestjs/testing
with supertest
to simulate real HTTP requests to your application.
app.e2e-spec.ts
tsCopyEditimport { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { AppModule } from './../src/app.module';
import * as request from 'supertest';
describe('App (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/users (POST)', async () => {
const res = await request(app.getHttpServer())
.post('/users')
.send({ email: '[email protected]' })
.expect(201);
expect(res.body.email).toEqual('[email protected]');
});
it('/users/:email (GET)', async () => {
await request(app.getHttpServer())
.get('/users/[email protected]')
.expect(200);
});
afterAll(async () => {
await app.close();
});
});
Note:
- Your controller should handle
/users
POST and/users/:email
GET for this to work. - Include TypeORM SQLite in
AppModule
for real DB interaction during E2E tests.
Best Practices
- Use unit tests for fast, logic-specific coverage.
- Use integration tests to verify repository interactions.
- Use E2E tests to validate request-response cycles.
- Clean up test DBs after each run to avoid pollution.
- Keep mocks in a
test/mocks/
directory and reuse them across tests.
Conclusion
In this article, we explored both unit and integration testing for services that use database access in NestJS. You learned how to:
- Use mocks for unit testing repository interactions.
- Set up SQLite in-memory DB for lightweight integration tests.
- Use
@nestjs/testing
andsupertest
to simulate real HTTP requests in E2E testing.
By combining these techniques, you can build a well-tested, stable, and production-ready NestJS backend.