Back to all posts

Writing Clean and Maintainable Tests in NestJS

S
sonhp
10 min read

Writing Clean and Maintainable Tests in NestJS

Testing is crucial for maintaining reliable and bug-free applications. However, poorly written tests can be just as harmful as no tests at all. In this guide, we’ll explore:

  • Avoiding flaky tests
  • Structuring unit and integration tests properly
  • Using mocks and spies effectively

1. Avoiding Flaky Tests

A flaky test is a test that sometimes passes and sometimes fails without code changes.
This leads to false confidence and wasted debugging time.


Common Causes of Flaky Tests

  • Tests dependent on external APIs or databases
  • Race conditions due to improper async handling
  • Shared global state between tests
  • Randomized data without predictable assertions

How to Avoid Flaky Tests

  1. Use dependency injection & mocks to avoid external API failures
  2. Ensure database transactions are properly rolled back in tests
  3. Use deterministic test data instead of random values
  4. Avoid testing implementation details – focus on behavior

2. Structuring Unit and Integration Tests Properly

In NestJS, we commonly write:

  • Unit tests – Test isolated components (services, utilities)
  • Integration tests – Test multiple components working together

Unit Test Example (UserService)

📌 user.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';

describe('UserService', () => {
  let userService: UserService;
  let userRepository: Partial<UserRepository>;

  beforeEach(async () => {
    userRepository = {
      findUserById: jest.fn().mockResolvedValue({ id: '1', name: 'John Doe' }),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        { provide: UserRepository, useValue: userRepository },
      ],
    }).compile();

    userService = module.get<UserService>(UserService);
  });

  it('should return a user by ID', async () => {
    const user = await userService.getUserById('1');
    expect(user).toEqual({ id: '1', name: 'John Doe' });
    expect(userRepository.findUserById).toHaveBeenCalledWith('1');
  });
});

Mocks dependencies using Jest
Uses a clean beforeEach setup
Tests only the behavior, not implementation details


Integration Test Example (UserController)

📌 user.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';

describe('UserController', () => {
  let userController: UserController;
  let userService: Partial<UserService>;

  beforeEach(async () => {
    userService = {
      getUserById: jest.fn().mockResolvedValue({ id: '1', name: 'John Doe' }),
    };

    const module: TestingModule = await Test.createTestingModule({
      controllers: [UserController],
      providers: [{ provide: UserService, useValue: userService }],
    }).compile();

    userController = module.get<UserController>(UserController);
  });

  it('should return user data', async () => {
    const user = await userController.getUser('1');
    expect(user).toEqual({ id: '1', name: 'John Doe' });
    expect(userService.getUserById).toHaveBeenCalledWith('1');
  });
});

Mocks UserService instead of using the real service
Keeps dependencies isolated
Ensures controller works correctly without needing an actual database


3. Using Mocks and Spies Effectively

When to Use Mocks?

Mocks help replace real dependencies with controlled implementations for testing.

Use Mocks for:

  • Databases (UserRepository) – Prevents real DB calls
  • External APIs (HttpService) – Avoids network failures
  • Services (UserService) – Keeps tests isolated

Mocking External API Calls

📌 http.service.spec.ts

import { HttpService } from '@nestjs/axios';
import { of } from 'rxjs';

describe('External API Call', () => {
  let httpService: HttpService;

  beforeEach(() => {
    httpService = new HttpService();
    jest
      .spyOn(httpService, 'get')
      .mockReturnValue(of({ data: { result: 'success' } }));
  });

  it('should call external API and return success', async () => {
    const result = await httpService
      .get('https://api.example.com/data')
      .toPromise();
    expect(result.data.result).toBe('success');
  });
});

Mocks external API requests with Jest spies
Avoids network dependency in tests


When to Use Spies?

A spy wraps a real function and tracks calls to it.

const spy = jest.spyOn(userService, 'getUserById');

Spies are useful for tracking function calls without overriding behavior


4. Summary: Best Practices for Clean Testing

Avoid flaky tests by using mocks and handling async properly
Structure unit and integration tests clearly
Use Jest mocks and spies to isolate dependencies
Test behavior, not implementation details

S

Written by sonhp

Technical writer and developer passionate about web technologies.

Related Articles