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