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

Author

Categories

Share

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.

Author

Share