Testing NestJS Applications

Writing unit tests, integration tests, and end-to-end tests using Jest and Supertest.


Mocking Dependencies in NestJS Tests

This document provides a comprehensive guide to mocking dependencies in NestJS tests using Jest. Mocking is crucial for writing effective unit tests that are isolated, deterministic, and fast. By mocking dependencies, you can isolate the component under test and verify its behavior without relying on the actual implementation of its dependencies.

Why Mock Dependencies?

  • Isolation: Prevents test failures due to issues in dependent services, repositories, or external APIs.
  • Determinism: Ensures tests produce consistent results regardless of the state of external systems.
  • Speed: Avoids slow database queries or network calls, leading to faster test execution.
  • Control: Allows you to simulate various scenarios and edge cases that might be difficult to reproduce in a real environment.

General Mocking Principles

The core idea behind mocking is to replace a real dependency with a simulated object that you can control. This simulated object, often called a "mock" or a "stub," allows you to:

  • Define return values: Specify what the mock should return for different method calls.
  • Track calls: Verify that methods on the mock were called with the expected arguments.
  • Simulate errors: Make the mock throw errors to test error handling logic.

Mocking Techniques in NestJS with Jest

NestJS provides excellent support for testing, especially when combined with Jest. Here are several common techniques for mocking dependencies:

1. Using `jest.fn()` to Create Mock Functions

`jest.fn()` creates a simple mock function. You can then define its behavior, such as its return value or the errors it throws.

Example: Mocking a Service Method

 // service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class MyService {
  getData(): string {
    return 'Real data from MyService';
  }

  async fetchDataFromExternalApi(id: number): Promise {
    // Simulate fetching data
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({ id: id, data: 'Some external data' });
      }, 100);
    });
  }
}

// controller.ts
import { Controller, Get } from '@nestjs/common';
import { MyService } from './service';

@Controller()
export class MyController {
  constructor(private readonly myService: MyService) {}

  @Get()
  getHello(): string {
    return this.myService.getData();
  }

  @Get('external')
  async getExternalData(): Promise {
    return this.myService.fetchDataFromExternalApi(123);
  }
}

// controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { MyController } from './controller';
import { MyService } from './service';

describe('MyController', () => {
  let myController: MyController;
  let myService: MyService;

  const mockMyService = {
    getData: jest.fn().mockReturnValue('Mocked data'),
    fetchDataFromExternalApi: jest.fn().mockResolvedValue({id: 123, data: 'Mocked external data'})
  };

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [MyController],
      providers: [
        {
          provide: MyService,
          useValue: mockMyService,
        },
      ],
    }).compile();

    myController = app.get(MyController);
    myService = app.get(MyService);
  });

  it('should return "Mocked data"', () => {
    expect(myController.getHello()).toBe('Mocked data');
    expect(mockMyService.getData).toHaveBeenCalled();
  });

  it('should return mocked external data', async () => {
    const result = await myController.getExternalData();
    expect(result).toEqual({id: 123, data: 'Mocked external data'});
    expect(mockMyService.fetchDataFromExternalApi).toHaveBeenCalledWith(123);
  });
}); 

Explanation:

  • We create a mockMyService object with mock functions for getData and fetchDataFromExternalApi.
  • We use jest.fn().mockReturnValue('Mocked data') to define the return value of getData.
  • We use jest.fn().mockResolvedValue({id: 123, data: 'Mocked external data'}) to define the resolved value of the promise returned by fetchDataFromExternalApi. This is crucial for asynchronous functions.
  • In the Test.createTestingModule, we provide this mock object using the useValue property.
  • We then verify that the controller returns the mocked data and that the mocked service methods are called.

2. Using `useFactory` for More Complex Mocking

The useFactory provider option allows for more complex mocking scenarios, such as dynamically generating mock objects or injecting dependencies into the mock.

Example: Mocking a Repository with Dependencies

 // repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserRepository {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository,
  ) {}

  async findOne(id: number): Promise {
    return this.userRepository.findOne({where: { id }});
  }

  async createUser(name: string): Promise {
    const user = this.userRepository.create({ name });
    return this.userRepository.save(user);
  }
}

// user.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './repository';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(private readonly userRepository: UserRepository) {}

  async getUser(id: number): Promise {
    return this.userRepository.findOne(id);
  }

  async createUser(name: string): Promise {
    return this.userRepository.createUser(name);
  }
}

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

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

  const mockUserRepository = {
    findOne: jest.fn().mockResolvedValue({ id: 1, name: 'Test User' }),
    createUser: jest.fn().mockResolvedValue({ id: 2, name: 'New User' })
  };

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

    userService = module.get(UserService);
    userRepository = module.get(UserRepository);
  });

  it('should be defined', () => {
    expect(userService).toBeDefined();
  });

  it('should get a user by ID', async () => {
    const user = await userService.getUser(1);
    expect(user).toEqual({ id: 1, name: 'Test User' });
    expect(mockUserRepository.findOne).toHaveBeenCalledWith(1);
  });

  it('should create a user', async () => {
      const user = await userService.createUser('New User');
      expect(user).toEqual({ id: 2, name: 'New User' });
      expect(mockUserRepository.createUser).toHaveBeenCalledWith('New User');
  });
}); 

Explanation:

  • We define a mockUserRepository object with mocked findOne and createUser methods.
  • Again, we are using mockResolvedValue to ensure asynchronous mock behavior.
  • We provide this mock object using useValue for the UserRepository provider.
  • The tests verify that the UserService calls the mocked repository methods with the correct arguments and returns the mocked data.

3. Using the `useClass` Provider

For more complex mocking scenarios, you can create a dedicated mock class that implements the same interface as the original class. This can be useful when you need more control over the mock's behavior or when you want to reuse the mock across multiple tests.

Example: Using a Mock Class

 // my.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class MyService {
  async fetchData(): Promise {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('Real data');
      }, 50);
    });
  }
}

// my.controller.ts
import { Controller, Get } from '@nestjs/common';
import { MyService } from './my.service';

@Controller()
export class MyController {
  constructor(private readonly myService: MyService) {}

  @Get()
  async getData(): Promise {
    return this.myService.fetchData();
  }
}

// my.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { MyService } from './my.service';
import { MyController } from './my.controller';

class MockMyService {
  async fetchData(): Promise {
    return 'Mocked data from MockMyService';
  }
}

describe('MyController', () => {
  let controller: MyController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [MyController],
      providers: [
        {
          provide: MyService,
          useClass: MockMyService,
        },
      ],
    }).compile();

    controller = module.get(MyController);
  });

  it('should return mocked data', async () => {
    expect(await controller.getData()).toBe('Mocked data from MockMyService');
  });
}); 

Explanation:

  • We define a MockMyService class that implements the same fetchData method as the original MyService.
  • We use useClass: MockMyService in the testing module to replace the original service with the mock class.
  • The test verifies that the controller returns the mocked data from the mock class.

4. Mocking External API Calls with `axios` or `node-fetch`

When your services make external API calls using libraries like `axios` or `node-fetch`, you need to mock these libraries to prevent actual network requests during tests. Jest provides excellent tools for mocking modules.

Example: Mocking `axios`

 // external.service.ts
import { Injectable } from '@nestjs/common';
import axios from 'axios';

@Injectable()
export class ExternalService {
  async fetchData(url: string): Promise {
    try {
      const response = await axios.get(url);
      return response.data;
    } catch (error) {
      console.error(error);
      throw error;
    }
  }
}

// external.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ExternalService } from './external.service';
import axios from 'axios';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked;

describe('ExternalService', () => {
  let service: ExternalService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [ExternalService],
    }).compile();

    service = module.get(ExternalService);
  });

  it('should fetch data from an external API', async () => {
    const mockData = { data: 'Mocked data from external API' };
    mockedAxios.get.mockResolvedValue({ data: mockData });

    const url = 'https://example.com/api/data';
    const result = await service.fetchData(url);

    expect(mockedAxios.get).toHaveBeenCalledWith(url);
    expect(result).toEqual(mockData);
  });

  it('should handle errors from the external API', async () => {
      mockedAxios.get.mockRejectedValue(new Error('API Error'));

      const url = 'https://example.com/api/data';

      await expect(service.fetchData(url)).rejects.toThrow('API Error');
      expect(mockedAxios.get).toHaveBeenCalledWith(url);
  });
}); 

Explanation:

  • We use jest.mock('axios') to mock the entire `axios` module.
  • const mockedAxios = axios as jest.Mocked; is important. It provides proper TypeScript typing for the mocked axios object, allowing you to use methods like mockResolvedValue and toHaveBeenCalledWith.
  • We then use mockedAxios.get.mockResolvedValue({ data: mockData }) to define the resolved value of the mocked axios.get method.
  • The tests verify that the fetchData method calls axios.get with the correct URL and returns the mocked data. We also test the error handling scenario using mockRejectedValue.

Important Considerations

  • Over-mocking: Avoid mocking too much. Focus on mocking only the external dependencies of the component under test. Over-mocking can lead to tests that are brittle and don't accurately reflect the behavior of the real system.
  • Type Safety: Use TypeScript's type system to your advantage when mocking. Define interfaces or types for your mocks to ensure that they are compatible with the original dependencies.
  • Clean Up Mocks: After each test, use jest.clearAllMocks() or jest.resetAllMocks() to reset the state of your mocks. This prevents state from leaking between tests. (Note: If you need to retain mock history for debugging purposes, use jest.restoreAllMocks() instead of resetting.)
     afterEach(() => {
                jest.clearAllMocks();
            }); 

Conclusion

Mocking is an essential skill for writing effective unit tests in NestJS. By using the techniques described in this document, you can create tests that are isolated, deterministic, and fast, ensuring that your NestJS applications are robust and reliable.