Testing is an essential part of software development, ensuring that your application behaves as expected and preventing bugs. NestJS provides excellent tools and support for both unit and integration testing, allowing you to write tests that ensure the reliability and correctness of your codebase.
In this module, we will dive deep into unit testing and integration testing in NestJS, covering how to test services, controllers, and how to set up a testing environment. We will also look at the best practices for writing effective tests.
Table of Contents
- Introduction to Testing in NestJS
- Setting Up the Testing Environment
- Unit Testing in NestJS
- Writing Tests for Services
- Mocking Dependencies
- Integration Testing in NestJS
- Setting Up Integration Tests
- Testing Controllers and Routes
- Using @nestjs/testing Module
- Best Practices for Testing in NestJS
- Conclusion
Introduction to Testing in NestJS
NestJS uses Jest as its default testing framework, which provides an easy-to-use, powerful testing environment. Jest supports unit tests, integration tests, mocking, and much more. Unit tests are designed to test individual parts of your application in isolation, while integration tests are used to test how components interact with each other.
In this module, we’ll explore both types of testing and demonstrate how to implement them effectively in a NestJS project.
Setting Up the Testing Environment
To get started with testing in NestJS, you must first set up the testing environment. NestJS uses the @nestjs/testing
package to facilitate the setup of unit and integration tests.
Install Dependencies
NestJS comes with Jest pre-configured. However, if you’re starting from scratch, ensure you have the necessary dependencies:
bashCopyEditnpm install --save-dev jest @nestjs/testing ts-jest @types/jest
You also need to configure your jest.config.js
file for NestJS if it’s not already set up:
jsCopyEditmodule.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
Unit Testing in NestJS
Unit tests are used to verify the behavior of individual units or components, such as services or methods, in isolation from other parts of the system. When writing unit tests, it’s essential to mock external dependencies to isolate the unit being tested.
Writing Tests for Services
Services in NestJS are typically where business logic resides, and they are an excellent candidate for unit testing. For example, if we have a UsersService
that contains a method for fetching user data, we can write unit tests for it.
Example: Unit Test for a Service
Consider a UsersService
with a findOne()
method:
tsCopyEdit@Injectable()
export class UsersService {
constructor(@InjectRepository(User) private userRepository: Repository<User>) {}
async findOne(id: string): Promise<User> {
return await this.userRepository.findOne(id);
}
}
To write a unit test for findOne
, we’ll mock the userRepository
dependency:
tsCopyEditimport { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';
describe('UsersService', () => {
let service: UsersService;
let mockUserRepository: Partial<Repository<User>>;
beforeEach(async () => {
mockUserRepository = {
findOne: jest.fn().mockResolvedValue({ id: '1', name: 'John' }),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockUserRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should return a user by ID', async () => {
const result = await service.findOne('1');
expect(result).toEqual({ id: '1', name: 'John' });
expect(mockUserRepository.findOne).toHaveBeenCalledWith('1');
});
});
In this test, we mock the userRepository
and test that the findOne()
method of the service correctly returns a user object.
Mocking Dependencies
Mocking dependencies allows us to isolate the service or component being tested. In the example above, we used Jest’s jest.fn()
method to mock the findOne
method of the repository.
This ensures that the actual database interaction is not triggered during testing, providing more control over the test environment.
Integration Testing in NestJS
Integration tests focus on testing the interaction between multiple components or systems. These tests are typically broader in scope and ensure that the parts of your application work together as expected.
Setting Up Integration Tests
To write integration tests in NestJS, we create an instance of the NestJS application using Test.createTestingModule()
, similar to how we set up unit tests. However, in integration tests, we aim to run the application in a test environment, where real or mocked modules are used to test how components interact.
tsCopyEditimport { Test, TestingModule } from '@nestjs/testing';
import { AppModule } from '../src/app.module';
import { INestApplication } from '@nestjs/common';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/GET users', async () => {
const response = await request(app.getHttpServer()).get('/users');
expect(response.status).toBe(200);
expect(response.body).toEqual(expect.arrayContaining([expect.objectContaining({ id: expect.any(String), name: expect.any(String) })]));
});
afterAll(async () => {
await app.close();
});
});
In the above example, we use supertest (integrated with Jest) to make HTTP requests to the app and validate the responses. The test checks whether the /GET users
endpoint returns the correct status and structure.
Testing Controllers and Routes
In integration tests, controllers are tested by sending HTTP requests to routes. This ensures that the controller correctly interacts with the services and provides the expected responses.
Example: Testing a Controller
tsCopyEditimport { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: {},
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
it('should return a user', async () => {
const user = { id: '1', name: 'John' };
jest.spyOn(service, 'findOne').mockResolvedValue(user);
expect(await controller.getUser('1')).toEqual(user);
});
});
This test checks that the UsersController
correctly delegates the logic to the UsersService
and returns the expected user object.
Using @nestjs/testing Module
The @nestjs/testing
module is a powerful tool for testing in NestJS. It provides utilities like:
createTestingModule()
: This method allows you to create a testing module with the same setup as your main application.get()
: It helps retrieve the provider (service, controller, etc.) to be tested.compile()
: Compiles the testing module, which initializes and prepares it for use.
This module ensures that unit and integration tests are straightforward and allow NestJS applications to be tested in an isolated and controlled environment.
Best Practices for Testing in NestJS
- Keep Tests Isolated: For unit tests, make sure that each test case is isolated from the others. Use mocking to ensure that you’re not testing external dependencies.
- Test Public APIs: When performing integration tests, focus on testing the public interfaces of the application, such as HTTP endpoints, rather than private implementation details.
- Use In-Memory Databases: For integration tests that require database interaction, use an in-memory database like SQLite or MongoDB in memory to keep tests fast and isolated from the production environment.
- Test Edge Cases: Always test edge cases, error handling, and failure scenarios to ensure robustness in your application.
- Run Tests on CI/CD: Make sure to integrate your tests into your Continuous Integration and Continuous Deployment pipeline to catch bugs early.
Conclusion
In this module, we explored unit testing and integration testing in NestJS, covering the setup, writing tests, and mocking dependencies. We also discussed how to test services, controllers, and routes using Jest and @nestjs/testing
.
By following best practices and leveraging the tools provided by NestJS, you can ensure that your application is robust, maintainable, and free of bugs. Testing is essential for delivering high-quality software, and NestJS offers a streamlined approach to writing tests with minimal setup.