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.

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