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 forgetData
andfetchDataFromExternalApi
. - We use
jest.fn().mockReturnValue('Mocked data')
to define the return value ofgetData
. - We use
jest.fn().mockResolvedValue({id: 123, data: 'Mocked external data'})
to define the resolved value of the promise returned byfetchDataFromExternalApi
. This is crucial for asynchronous functions. - In the
Test.createTestingModule
, we provide this mock object using theuseValue
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 mockedfindOne
andcreateUser
methods. - Again, we are using
mockResolvedValue
to ensure asynchronous mock behavior. - We provide this mock object using
useValue
for theUserRepository
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 samefetchData
method as the originalMyService
. - 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
andtoHaveBeenCalledWith
.- We then use
mockedAxios.get.mockResolvedValue({ data: mockData })
to define the resolved value of the mockedaxios.get
method. - The tests verify that the
fetchData
method callsaxios.get
with the correct URL and returns the mocked data. We also test the error handling scenario usingmockRejectedValue
.
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()
orjest.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, usejest.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.