NestJS — Enterprise Node.js

Unit Testing Services & Controllers

16 min Lesson 43 of 48

Unit Testing Services & Controllers

Unit tests verify a single class in complete isolation — no real databases, no HTTP servers, no external dependencies. NestJS ships with Jest pre-configured and provides Test.createTestingModule(), a lightweight module factory that compiles only what you declare, replacing anything real with your own mock providers. The result is tests that run in milliseconds and give precise, reproducible feedback.

Why isolate with mocks?

  • Speed — no I/O means the full suite finishes in seconds.
  • Precision — a failing test points at one class, not an integration chain.
  • Determinism — you control every dependency's return value; flaky network issues never break CI.
  • Design pressure — code that is hard to unit-test is usually hard to maintain; pain here is signal.

Creating an isolated testing module

Test.createTestingModule() accepts the same metadata object as @Module(). Declare only the class under test and provide mock replacements for every dependency using the custom provider syntax (useValue):

import { 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; const mockRepo = { findOne: jest.fn(), save: jest.fn(), create: jest.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(User), useValue: mockRepo, }, ], }).compile(); service = module.get<UsersService>(UsersService); }); afterEach(() => jest.clearAllMocks()); it('should be defined', () => { expect(service).toBeDefined(); }); });
compile() is async. Always await it inside a beforeEach async callback. The compiled module is ephemeral — each test gets a fresh instance, preventing state bleed between tests.

Testing service methods with spies

Use jest.fn() to declare a mock function, and mockResolvedValue / mockReturnValue to control what it returns for a specific test. Then spy on the mock to assert it was called with the right arguments:

it('should return a user when found', async () => { const fakeUser = { id: 1, email: 'a@example.com' }; mockRepo.findOne.mockResolvedValue(fakeUser); const result = await service.findById(1); expect(result).toEqual(fakeUser); expect(mockRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } }); expect(mockRepo.findOne).toHaveBeenCalledTimes(1); }); it('should throw NotFoundException when user is missing', async () => { mockRepo.findOne.mockResolvedValue(null); await expect(service.findById(99)).rejects.toThrow('User not found'); });

Unit-testing a controller

Controllers depend on services, not repositories. Provide a mock service with useValue, compile the module, and call the controller methods directly — no HTTP overhead:

import { UsersController } from './users.controller'; describe('UsersController', () => { let controller: UsersController; const mockUsersService = { findById: jest.fn(), create: jest.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], providers: [ { provide: UsersService, useValue: mockUsersService }, ], }).compile(); controller = module.get<UsersController>(UsersController); }); it('should call service.findById and return the result', async () => { const user = { id: 1, email: 'a@example.com' }; mockUsersService.findById.mockResolvedValue(user); const result = await controller.findOne('1'); expect(result).toEqual(user); expect(mockUsersService.findById).toHaveBeenCalledWith(1); }); });

jest.spyOn for partial mocking

When you want to mock only one method on a real object — leaving the rest intact — use jest.spyOn(object, 'methodName'). This is ideal for testing interactions between methods inside the same class:

it('should call hashPassword before saving', async () => { const spy = jest.spyOn(service as any, 'hashPassword') .mockResolvedValue('hashed-pw'); mockRepo.create.mockReturnValue({ email: 'b@b.com' }); mockRepo.save.mockResolvedValue({ id: 2, email: 'b@b.com' }); await service.create({ email: 'b@b.com', password: 'plain' }); expect(spy).toHaveBeenCalledWith('plain'); });
Keep mock objects shallow. Only declare the methods the class under test actually calls. A leaner mock object makes the test's dependency surface explicit and the error message when a method is missing far more helpful.
Never test implementation details. Assert on observable outcomes (return values, thrown errors, which collaborators were called). Asserting on how a method is internally structured makes tests brittle — they break on safe refactors.

Summary

Test.createTestingModule() compiles an isolated NestJS module for testing. Replace every real dependency with a useValue mock built from jest.fn() functions, then use mockResolvedValue to control return values and toHaveBeenCalledWith to verify interactions. Apply the same pattern to controllers — provide a mock service instead of the real one. Use jest.spyOn for targeted partial mocking. Unit tests should be fast, isolated, and focused on a single class at a time.