Welcome back to the Securing and Testing Your MVC NestJS App course! Previously, we covered session management and user authentication. Now, let's ensure our code is robust and error-free through unit testing with mocks.
By the end of this lesson, you'll be able to:
- Understand the significance of unit testing.
- Set up unit testing in a NestJS project.
- Write unit tests for controllers and services.
- Implement mocks to test services dependent on external resources, such as databases.
Unit tests validate individual components, or units, of your application in isolation. These tests focus on small functions, classes, or methods to ensure they work correctly on their own.
Key Characteristics of Unit Tests:
- Isolated: Unit tests do not rely on external systems like databases or APIs. When a service or method interacts with such systems, you use mocks to simulate those interactions.
- Repeatable: Unit tests should produce consistent results every time they run.
- Fast: Since they only test individual parts of the application, they run quickly.
Boilerplate Setup
To begin, you need to import essential modules from NestJS's testing utilities, along with the AppController
and AppService
. This setup is crucial to prepare the testing environment and ensure that necessary components are available for testing:
TypeScript1import { Test, TestingModule } from '@nestjs/testing'; 2import { AppController } from './app.controller'; 3import { AppService } from './app.service';
Setting Up the Test Module
Here, we set up the testing environment by using Test.createTestingModule
to create a module. This module includes the AppController
and AppService
as providers, which ensures that each test runs in a controlled, isolated environment, minimizing external dependencies:
TypeScript1describe('AppController', () => { 2 let appController: AppController; 3 4 beforeEach(async () => { 5 const app: TestingModule = await Test.createTestingModule({ 6 controllers: [AppController], 7 providers: [AppService], 8 }).compile(); 9 10 appController = app.get<AppController>(AppController); 11 });
Writing a Test Case
In this step, we define a test case within the describe
block. This test verifies the behavior of the getHello()
method, ensuring it returns 'Hello World!'
. This case demonstrates the syntax and structure of basic NestJS tests:
TypeScript1 describe('getHello', () => { 2 it('should return "Hello World!"', () => { 3 expect(appController.getHello()).toBe('Hello World!'); 4 }); 5 }); 6});
You can run this test by executing the following command in your terminal:
Bash1npm run test
This command runs the unit tests that have been defined in your project, confirming that your controller works as expected if everything is set up correctly.
Mocks are simulated versions of real objects, like databases or external services, used to control behavior during tests. They allow developers to isolate the test code from external dependencies, ensuring tests remain predictable and fast.
Creating the User Entity
The User
entity defines the structure of user objects saved in a database. This entity is equipped with fields like id
, username
, and password
, and is decorated to map to a database table:
TypeScript1import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 3@Entity() 4export class User { 5 @PrimaryGeneratedColumn() 6 id: number; 7 8 @Column() 9 username: string; 10 11 @Column() 12 password: string; 13}
UserService Implementation
This service contains methods for performing CRUD operations on the User
entity, interacting with the database via the repository pattern. It's a typical pattern in NestJS applications:
TypeScript1import { Injectable } from '@nestjs/common'; 2import { InjectRepository } from '@nestjs/typeorm'; 3import { Repository } from 'typeorm'; 4import { User } from './user.entity'; 5 6@Injectable() 7export class UserService { 8 constructor( 9 @InjectRepository(User) private userRepository: Repository<User>, 10 ) {} 11 12 async findAll(): Promise<User[]> { 13 return this.userRepository.find(); 14 } 15 16 async findOne(id: number): Promise<User> { 17 return this.userRepository.findOneBy({ id }); 18 } 19 20 async create(username: string, password: string): Promise<User> { 21 const user = this.userRepository.create({ username, password }); 22 return this.userRepository.save(user); 23 } 24}
Unit Test Setup
We mock the User
repository using the getRepositoryToken()
function. This enables the testing of UserService
independently of a real database, illustrating the essential setup for effective unit testing in NestJS:
TypeScript1import { Test, TestingModule } from '@nestjs/testing'; 2import { UserService } from './user.service'; 3import { User } from './user.entity'; 4import { Repository } from 'typeorm'; 5import { getRepositoryToken } from '@nestjs/typeorm'; 6 7describe('UserService', () => { 8 let userService: UserService; 9 let userRepository: Repository<User>; 10 11 beforeEach(async () => { 12 const mockRepository = { 13 find: jest.fn(), 14 findOneBy: jest.fn(), 15 create: jest.fn(), 16 save: jest.fn(), 17 }; 18 19 const module: TestingModule = await Test.createTestingModule({ 20 providers: [ 21 UserService, 22 { 23 provide: getRepositoryToken(User), 24 useValue: mockRepository, 25 }, 26 ], 27 }).compile(); 28 29 userService = module.get<UserService>(UserService); 30 userRepository = module.get<Repository<User>>(getRepositoryToken(User)); 31 });
Defining Service Instance
This simple test case checks that the UserService
instance is correctly defined and ready for use. It's a critical initial step in confirming setup correctness:
TypeScript1 it('should be defined', () => { 2 expect(userService).toBeDefined(); 3 });
This function checks the behavior of the findAll
method, verifying it returns an array of users as expected. The test uses mocked data to simulate database operations:
TypeScript1 describe('findAll', () => { 2 it('should return an array of users', async () => { 3 const expectedUsers = [{ id: 1, username: 'john', password: 'changeme' }]; 4 (userRepository.find as jest.Mock).mockResolvedValue(expectedUsers); 5 6 const users = await userService.findAll(); 7 expect(users).toEqual(expectedUsers); 8 expect(userRepository.find).toHaveBeenCalledTimes(1); 9 }); 10 });
Here, the test verifies that the findOne
method retrieves a specific user by their ID. It confirms the method functions correctly even with mocked database interactions:
TypeScript1 describe('findOne', () => { 2 it('should return a single user', async () => { 3 const expectedUser = { id: 1, username: 'john', password: 'changeme' }; 4 (userRepository.findOneBy as jest.Mock).mockResolvedValue(expectedUser); 5 6 const user = await userService.findOne(1); 7 expect(user).toEqual(expectedUser); 8 expect(userRepository.findOneBy).toHaveBeenCalledWith({ id: 1 }); 9 }); 10 });
This test ensures the create
method properly adds a new user and ensures it gets saved in the repository. It checks that both creating and saving functions are called with the right parameters:
TypeScript1 describe('create', () => { 2 it('should create and return a user', async () => { 3 const userInput = { username: 'jane', password: 'secret' }; 4 const savedUser = { id: 2, ...userInput }; 5 6 (userRepository.create as jest.Mock).mockReturnValue(userInput); 7 (userRepository.save as jest.Mock).mockResolvedValue(savedUser); 8 9 const user = await userService.create(userInput.username, userInput.password); 10 expect(user).toEqual(savedUser); 11 expect(userRepository.create).toHaveBeenCalledWith(userInput); 12 expect(userRepository.save).toHaveBeenCalledWith(userInput); 13 }); 14 }); 15});
Unit testing is an essential part of building reliable and maintainable applications. By writing unit tests, you ensure that individual parts of your application behave correctly in isolation.
- Early Bug Detection: Unit tests help catch errors early in development, reducing the effort and cost of fixing them later.
- Refactoring Confidence: Unit tests provide confidence to refactor or improve code, knowing any issues introduced will be detected.
- Documentation: Unit tests act as documentation, illustrating how a function, service, or controller is expected to behave.
- Code Quality: Writing tests encourages better code design and structure, making your application more maintainable and scalable.
Mocks enable you to test these units in isolation without relying on real services like databases or external APIs. They allow for fast, repeatable testing during the development process, providing assurance that your application’s components work as intended.
Now it's time to write your own tests! Let's move to the practice section and start applying what you've learned!