Testing NestJS Applications
Writing unit tests, integration tests, and end-to-end tests using Jest and Supertest.
Writing Unit Tests for NestJS Components
This guide provides a detailed explanation of writing effective unit tests for individual components (Services, Controllers, etc.) within a NestJS application. It focuses on isolating components, mocking dependencies, and asserting expected behavior.
Understanding Unit Testing in NestJS
Unit testing involves testing individual units (components) of your application in isolation. In NestJS, this typically means testing Services, Controllers, and other injectable providers independently. The key goal is to verify that each component functions correctly according to its intended purpose without relying on the behavior of its dependencies. This helps identify bugs early in the development process and ensures code reliability.
Key Principles of Unit Testing:
- Isolation: Each unit test should focus on a single component and isolate it from its dependencies.
- Mocking: Dependencies of the component under test should be replaced with mock objects or stubs to control their behavior.
- Assertion: Verify that the component produces the expected output or performs the expected actions based on the given input.
- Fast and Repeatable: Unit tests should execute quickly and produce consistent results.
Setting up Your Testing Environment
NestJS provides built-in support for testing using Jest and Supertest. You'll typically use the NestJS CLI to generate test files when creating components. Verify that you have the necessary testing dependencies installed. The `@nestjs/testing` module is crucial for creating test modules and mocking providers.
Example:
npm install --save-dev jest ts-jest @types/jest supertest @types/supertest
Ensure your jest.config.js
(or equivalent) file is properly configured. A typical configuration includes:
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
};
Testing Services
Services encapsulate business logic in NestJS. When testing a service, you'll typically mock any repository, external API client, or other service dependencies.
Example: Testing a UserService
Let's assume we have a UserService
that depends on a UserRepository
:
// src/user/user.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async findOne(id: number): Promise<User | undefined> {
return this.userRepository.findOne(id);
}
async createUser(name: string, email: string): Promise<User> {
const newUser = new User();
newUser.name = name;
newUser.email = email;
return this.userRepository.save(newUser);
}
}
// src/user/user.repository.ts
import { Injectable } from '@nestjs/common';
import { User } from './user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UserRepository {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async findOne(id: number): Promise<User | undefined> {
return this.userRepository.findOneBy({ id });
}
async save(user: User): Promise<User> {
return this.userRepository.save(user);
}
}
// src/user/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
}
Here's how you might write a unit test for the UserService
:
// src/user/user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
describe('UserService', () => {
let userService: UserService;
let userRepository: UserRepository;
let mockRepository: Repository<User>;
const mockUser = { id: 1, name: 'Test User', email: 'test@example.com' };
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
UserRepository,
{
provide: getRepositoryToken(User),
useValue: {
findOneBy: jest.fn().mockResolvedValue(mockUser),
save: jest.fn().mockResolvedValue(mockUser),
},
},
],
}).compile();
userService = module.get<UserService>(UserService);
userRepository = module.get<UserRepository>(UserRepository);
mockRepository = module.get<Repository<User>>(getRepositoryToken(User));
});
it('should be defined', () => {
expect(userService).toBeDefined();
});
describe('findOne', () => {
it('should return a user if found', async () => {
jest.spyOn(mockRepository, 'findOneBy').mockResolvedValue(mockUser);
const user = await userService.findOne(1);
expect(user).toEqual(mockUser);
});
it('should return undefined if user is not found', async () => {
jest.spyOn(mockRepository, 'findOneBy').mockResolvedValue(undefined);
const user = await userService.findOne(1);
expect(user).toBeUndefined();
});
});
describe('createUser', () => {
it('should create a new user', async () => {
const newUser = { name: 'New User', email: 'new@example.com' };
jest.spyOn(mockRepository, 'save').mockResolvedValue({ id: 2, ...newUser });
const createdUser = await userService.createUser(newUser.name, newUser.email);
expect(createdUser).toEqual({ id: 2, ...newUser });
expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining(newUser));
});
});
});
Explanation of the Service Test:
- Setup:
- We use
Test.createTestingModule
to create a testing module, mocking theUserRepository
using theuseValue
option. This allows us to control the behavior of the repository without actually hitting a database. - We get instances of the
UserService
and the mocked Repository from the testing module.
- We use
- Mocking:
- We are mocking the `findOneBy` and `save` methods of the `mockRepository` using `jest.fn().mockResolvedValue()`. This simulates the repository returning a specific user object.
- Assertions:
- In the
findOne
test, we assert that the service returns the mocked user when the repository returns the mock user. We also test the scenario where the user is not found (repository returnsundefined
). - In the
createUser
test, we verify that the service calls the repository'ssave
method with the correct user data and that the service returns the newly created user. We useexpect.objectContaining
to ensure that the saved object contains the expected properties. - We use `jest.spyOn` to observe that the method `save` was called, and verify the argument.
- In the
Testing Controllers
Controllers handle incoming requests and route them to appropriate services. When testing a controller, you'll typically mock the service dependencies and verify that the controller calls the service methods correctly and returns the expected response.
Example: Testing a UserController
Let's assume we have a UserController
that depends on the UserService
:
// src/user/user.controller.ts
import { Controller, Get, Param, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { User } from './user.entity';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
async findOne(@Param('id') id: string): Promise<User | undefined> {
return this.userService.findOne(Number(id));
}
@Post()
async createUser(@Body() body: { name: string; email: string }): Promise<User> {
return this.userService.createUser(body.name, body.email);
}
}
Here's how you might write a unit test for the UserController
:
// src/user/user.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { User } from './user.entity';
describe('UserController', () => {
let userController: UserController;
let userService: UserService;
const mockUser = { id: 1, name: 'Test User', email: 'test@example.com' };
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
{
provide: UserService,
useValue: {
findOne: jest.fn().mockResolvedValue(mockUser),
createUser: jest.fn().mockResolvedValue(mockUser),
},
},
],
}).compile();
userController = module.get<UserController>(UserController);
userService = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(userController).toBeDefined();
});
describe('findOne', () => {
it('should return a user', async () => {
jest.spyOn(userService, 'findOne').mockResolvedValue(mockUser);
const result = await userController.findOne('1');
expect(result).toEqual(mockUser);
expect(userService.findOne).toHaveBeenCalledWith(1);
});
});
describe('createUser', () => {
it('should create a new user', async () => {
const newUser = { name: 'New User', email: 'new@example.com' };
jest.spyOn(userService, 'createUser').mockResolvedValue(mockUser);
const result = await userController.createUser(newUser);
expect(result).toEqual(mockUser);
expect(userService.createUser).toHaveBeenCalledWith(newUser.name, newUser.email);
});
});
});
Explanation of the Controller Test:
- Setup:
- We use
Test.createTestingModule
to create a testing module, mocking theUserService
using theuseValue
option. - We get instances of the
UserController
and the mockedUserService
from the testing module.
- We use
- Mocking:
- We are mocking the
findOne
andcreateUser
methods of theUserService
usingjest.fn().mockResolvedValue()
.
- We are mocking the
- Assertions:
- In the
findOne
test, we assert that the controller returns the mocked user when the service returns the mock user. We also verify that the controller calls the service'sfindOne
method with the correct ID (converted to a number). - In the
createUser
test, we verify that the controller calls the service'screateUser
method with the correct user data and that the controller returns the expected user. - We use `jest.spyOn` and `toHaveBeenCalledWith` to observe that the method `createUser` and `findOne` was called, and verify the argument.
- In the
Best Practices for Effective Unit Tests
- Write tests before or alongside your code: Test-Driven Development (TDD) can help guide your design and ensure that your code is testable.
- Keep tests focused: Each test should target a specific behavior or scenario.
- Use descriptive test names: Test names should clearly indicate what the test is verifying (e.g., "should return a user if found").
- Avoid complex mocking: If you find yourself writing overly complex mocks, it might indicate that your component has too many responsibilities or dependencies.
- Test edge cases and error conditions: Ensure your component handles invalid input, exceptions, and other potential errors gracefully.
- Keep tests independent: Tests should not rely on the state or outcome of other tests. Use
beforeEach
andafterEach
hooks to reset the environment if necessary. - Use meaningful assertions: Use appropriate assertion methods (e.g.,
expect(value).toBe(expected)
,expect(value).toEqual(expected)
,expect(function).toThrow()
) to clearly express the expected outcome. - Review and refactor tests: As your codebase evolves, keep your tests up-to-date and refactor them to maintain readability and effectiveness.
- Use Parameterized tests: For the same logic with multiple inputs/outputs, you can create a single test case.
Common Mocking Techniques
jest.fn()
: Creates a simple mock function.jest.spyOn()
: Spies on a real method of an object, allowing you to track its calls and mock its implementation.mockResolvedValue()
: Simulates a Promise that resolves with a specific value.mockRejectedValue()
: Simulates a Promise that rejects with a specific error.useValue
inTest.createTestingModule
: Provides a mock implementation for a dependency in the testing module.
Conclusion
Writing effective unit tests is essential for building robust and maintainable NestJS applications. By following the principles of isolation, mocking, and assertion, you can create comprehensive tests that verify the behavior of your individual components and ensure the quality of your code.