Writing Unit and Integration Tests for Services with Database Access in NestJS

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

  1. Why Test Services with DB Access?
  2. Unit Testing: Isolated Service Logic
  3. Integration Testing with Real Database
  4. End-to-End (E2E) Testing with HTTP Requests
  5. Best Practices
  6. 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.

// test/mocks/user-repository.mock.ts
export const mockUserRepository = () => ({
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
});

Writing Unit Tests with Jest

// 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.

// 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

import { 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 and supertest to simulate real HTTP requests in E2E testing.

By combining these techniques, you can build a well-tested, stable, and production-ready NestJS backend.