Lesson 4
Unit Testing with Mocks
Introduction to Unit Testing with Mocks

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.

What You’ll Learn

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.
What Are Unit Tests?

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.
Setting Up Unit Tests in NestJS

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:

TypeScript
1import { 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:

TypeScript
1describe('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:

TypeScript
1 describe('getHello', () => { 2 it('should return "Hello World!"', () => { 3 expect(appController.getHello()).toBe('Hello World!'); 4 }); 5 }); 6});
Running the Test

You can run this test by executing the following command in your terminal:

Bash
1npm 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.

Understanding Mocks

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.

User Entity and Service Setup

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:

TypeScript
1import { 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:

TypeScript
1import { 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}
Writing Unit Tests for UserService with Mocks

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:

TypeScript
1import { 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 });
Writing Test Cases for the UserService

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:

TypeScript
1 it('should be defined', () => { 2 expect(userService).toBeDefined(); 3 });
Testing findAll Method

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:

TypeScript
1 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 });
Testing findOne Method

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:

TypeScript
1 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 });
Testing create Method

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:

TypeScript
1 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});
Why Unit Testing Is Important

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!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.