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:

  1. Setup:
    • We use Test.createTestingModule to create a testing module, mocking the UserRepository using the useValue 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.
  2. 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.
  3. 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 returns undefined).
    • In the createUser test, we verify that the service calls the repository's save method with the correct user data and that the service returns the newly created user. We use expect.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.

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:

  1. Setup:
    • We use Test.createTestingModule to create a testing module, mocking the UserService using the useValue option.
    • We get instances of the UserController and the mocked UserService from the testing module.
  2. Mocking:
    • We are mocking the findOne and createUser methods of the UserService using jest.fn().mockResolvedValue().
  3. 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's findOne method with the correct ID (converted to a number).
    • In the createUser test, we verify that the controller calls the service's createUser 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.

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 and afterEach 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 in Test.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.