NestJS Testing Patterns: Unit & Integration Tests

NestJSTypeScriptTestingBest Practices

Introduction

Testing is crucial for maintaining reliable NestJS applications. This guide covers practical testing patterns I use in production.

Unit Testing Services

Basic Service Test

import { Test } from '@nestjs/testing';
import { UserService } from './user.service';
import { getModelToken } from '@nestjs/mongoose';

describe('UserService', () => {
  let service: UserService;
  let model: Model<User>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: getModelToken(User.name),
          useValue: {
            find: jest.fn(),
            findById: jest.fn(),
            create: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<UserService>(UserService);
    model = module.get(getModelToken(User.name));
  });

  it('should find user by id', async () => {
    const mockUser = { id: '1', name: 'John' };
    jest.spyOn(model, 'findById').mockResolvedValue(mockUser);

    const result = await service.findById('1');
    expect(result).toEqual(mockUser);
  });
});

Mocking Dependencies

External API Mocking

describe('PaymentService', () => {
  let service: PaymentService;
  let httpService: HttpService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        PaymentService,
        {
          provide: HttpService,
          useValue: {
            post: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<PaymentService>(PaymentService);
    httpService = module.get<HttpService>(HttpService);
  });

  it('should process payment successfully', async () => {
    const mockResponse = { data: { status: 'success' } };
    jest.spyOn(httpService, 'post').mockReturnValue(of(mockResponse));

    const result = await service.processPayment({ amount: 100 });
    expect(result.status).toBe('success');
  });
});

Integration Testing

E2E Controller Test

import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('UserController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/users (GET)', () => {
    return request(app.getHttpServer())
      .get('/users')
      .expect(200)
      .expect((res) => {
        expect(res.body).toBeInstanceOf(Array);
      });
  });

  afterAll(async () => {
    await app.close();
  });
});

Testing Best Practices

1. Use Test Fixtures

const createMockUser = (overrides = {}) => ({
  id: '1',
  email: 'test@example.com',
  name: 'Test User',
  ...overrides,
});

2. Test Error Cases

it('should throw error when user not found', async () => {
  jest.spyOn(model, 'findById').mockResolvedValue(null);
  
  await expect(service.findById('invalid'))
    .rejects
    .toThrow(NotFoundException);
});

3. Test Guards and Interceptors

it('should deny access without JWT', () => {
  return request(app.getHttpServer())
    .get('/protected')
    .expect(401);
});

Key Takeaways

  • Mock external dependencies
  • Test both success and error cases
  • Use fixtures for test data
  • Write integration tests for critical flows
  • Maintain high test coverage (80%+)