Unit Testing with the Testing Module

Use Test.createTestingModule to build an isolated DI container and inject mocks with custom providers.

Nest's dependency injection is what makes it so testable. In a unit test you spin up a miniature DI container containing just the class under test, with every dependency replaced by a mock. No HTTP, no database, microsecond-fast.

Test.createTestingModule

It mirrors @Module(): list your real provider, then override each of its dependencies with a useValue mock. Call .compile() and pull the instance out with module.get().

const module = await Test.createTestingModule({
  providers: [
    UsersService,
    { provide: UsersRepository, useValue: mockRepo },
  ],
}).compile();

Mock at the token, not the import

Because you inject by token, you swap the dependency by re-registering the same token with a fake. The class under test never knows the difference - it just receives the mock the container hands it. This is why constructor injection beats importing singletons directly.

overrideProvider for bigger graphs

When you import a whole module, .overrideProvider(Token).useValue(mock) surgically replaces one provider while keeping the rest real. Handy for testing a service with its actual collaborators but a fake database.

Assert on behavior

Verify what the service did: the value it returned and the calls it made on its mocks (expect(mockRepo.save).toHaveBeenCalledWith(...)). Testing behavior rather than implementation keeps tests green through refactors.

Example

Example · typescript
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';

interface User {
  id: string;
  email: string;
}

class UsersRepository {
  findById(_id: string): Promise<User | null> {
    throw new Error('not implemented');
  }
}

class UsersService {
  constructor(private readonly repo: UsersRepository) {}
  async findOne(id: string): Promise<User> {
    const user = await this.repo.findById(id);
    if (!user) throw new NotFoundException(id);
    return user;
  }
}

describe('UsersService', () => {
  let service: UsersService;
  const repo = { findById: jest.fn() };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        // same token, fake implementation
        { provide: UsersRepository, useValue: repo },
      ],
    }).compile();

    service = module.get(UsersService);
    jest.resetAllMocks();
  });

  it('returns the user when found', async () => {
    repo.findById.mockResolvedValue({ id: '1', email: '[email protected]' });
    await expect(service.findOne('1')).resolves.toEqual({
      id: '1',
      email: '[email protected]',
    });
    expect(repo.findById).toHaveBeenCalledWith('1');
  });

  it('throws NotFound when missing', async () => {
    repo.findById.mockResolvedValue(null);
    await expect(service.findOne('x')).rejects.toBeInstanceOf(
      NotFoundException,
    );
  });
});

When to use it

  • A developer uses Test.createTestingModule to unit-test OrdersService with a mock repository so tests run without a database connection.
  • A team injects mock implementations for all external dependencies (email, payment) so unit tests are fast, deterministic, and runnable in CI without secrets.
  • An engineer uses jest.spyOn to verify that a specific service method was called with the expected arguments inside a controller unit test.

More examples

Service unit test with mock repo

Creates an isolated testing module with a mock TypeORM repository so UsersService methods can be tested without a real database.

Example · ts
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { User } from './user.entity';

const mockRepo = {
  find: jest.fn(() => [{ id: 1, name: 'Alice' }]),
  findOneBy: jest.fn((q: any) => ({ id: q.id, name: 'Alice' })),
};

const module = await Test.createTestingModule({
  providers: [
    UsersService,
    { provide: getRepositoryToken(User), useValue: mockRepo },
  ],
}).compile();

const service = module.get(UsersService);
expect(await service.findAll()).toHaveLength(1);

Verify method calls with spyOn

Uses jest.spyOn to assert that the controller delegates to service.findAll exactly once, verifying the delegation contract.

Example · ts
describe('UsersController', () => {
  let controller: UsersController;
  let service: UsersService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [{ provide: UsersService, useValue: { findAll: jest.fn(() => []) } }],
    }).compile();
    controller = module.get(UsersController);
    service = module.get(UsersService);
  });

  it('calls service.findAll', async () => {
    const spy = jest.spyOn(service, 'findAll');
    await controller.findAll();
    expect(spy).toHaveBeenCalledTimes(1);
  });
});

Test guard canActivate in isolation

Unit tests a guard in complete isolation by constructing a fake ExecutionContext object, no testing module needed.

Example · ts
import { ExecutionContext } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

const guard = new AuthGuard();

const createContext = (headers: Record<string, string>) => ({
  switchToHttp: () => ({
    getRequest: () => ({ headers }),
  }),
} as unknown as ExecutionContext);

it('allows request with valid API key', () => {
  process.env.API_KEY = 'secret';
  expect(guard.canActivate(createContext({ 'x-api-key': 'secret' }))).toBe(true);
});

it('rejects request without API key', () => {
  expect(guard.canActivate(createContext({}))).toBe(false);
});

Discussion

  • Be the first to comment on this lesson.