Unit Testing a Provider
Build a tiny DI container with Test.createTestingModule, pull one class out of it, and replace its dependencies with doubles.
const moduleRef: TestingModule = await Test.createTestingModule({ providers: [...] }).compile();A unit test for a provider starts no server and sends no HTTP request. It builds a small dependency-injection container, takes one class out of it, and calls that class's methods directly.
Two terms first. A provider is anything Nest can inject: usually a class marked @Injectable(), sometimes a plain value. A token is the key the container stores that provider under — the class itself for a class provider, or a string or symbol when the code uses @Inject('SOMETHING').
How it works
Test.createTestingModule() takes the same metadata object as @Module(): providers, controllers, imports. It returns a builder. await builder.compile() scans that metadata, resolves the dependency graph, constructs every instance and returns a TestingModule. moduleRef.get(token) then hands you an instance. On a TestingModule the strict option defaults to false, so get() searches the whole graph, not only the root module.
Declare the class under test for real, and bind each of its dependencies to a double: an object with the same method names, built from vi.fn(). Leave one unbound and compile() throws Nest can't resolve dependencies.
The same binding has two spellings. { provide: PaymentsService, useValue: double } inside providers, and .overrideProvider(PaymentsService).useValue(double) on the builder, produce exactly the same container entry. Use the first when you write the provider list yourself, the second when the provider arrives from an imported module.
What trips people up
compile() constructs instances but runs no lifecycle hooks.
Example
// src/orders/orders.service.spec.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { OrdersService } from './orders.service.js';
import { PaymentsService } from '../payments/payments.service.js';
import { ORDERS_REPOSITORY } from './orders.repository.js';
describe('OrdersService', () => {
let moduleRef: TestingModule;
let service: OrdersService;
// The doubles: same method names as the real collaborators, no network, no database.
const payments = { charge: vi.fn() };
const repository = { findById: vi.fn(), save: vi.fn() };
beforeEach(async () => {
// The doubles live outside beforeEach, so their recorded calls must be wiped
// or assertions leak from one test into the next.
vi.clearAllMocks();
moduleRef = await Test.createTestingModule({
providers: [
OrdersService, // the real class under test
{ provide: PaymentsService, useValue: payments }, // bound by class token
{ provide: ORDERS_REPOSITORY, useValue: repository }, // bound by symbol token
],
}).compile();
// Shorthand. The longhand is moduleRef.get(OrdersService, { strict: false }),
// and strict is already false on a TestingModule, so the two are identical.
service = moduleRef.get(OrdersService);
});
it('charges the card and stores the order as paid', async () => {
repository.findById.mockResolvedValue({ id: 'o1', total: 2500, paid: false });
// mockResolvedValue(x) is shorthand for mockImplementation(() => Promise.resolve(x)).
payments.charge.mockResolvedValue({ id: 'ch_1' });
await service.pay('o1');
expect(payments.charge).toHaveBeenCalledWith(2500);
expect(repository.save).toHaveBeenCalledWith(
expect.objectContaining({ id: 'o1', paid: true }),
);
});
it('does not charge anything for an unknown order', async () => {
repository.findById.mockResolvedValue(null);
await expect(service.pay('missing')).rejects.toThrow(NotFoundException);
expect(payments.charge).not.toHaveBeenCalled();
});
});When to use it
- Cover a service's branching - retry, discount, permission check - without starting Postgres.
- Prove an order is charged exactly once, by asserting on the calls recorded by the payment double.
- Reproduce a production bug as a failing spec before changing any code.
- Assert a provider throws NotFoundException for a row that does not exist.
More examples
Letting useMocker fill the gaps
useMocker(factory) registers one factory that Nest consults for every missing dependency instead of a provider entry per collaborator. Reach for it when the class under test has six dependencies and the test only cares about one of them; the trade-off is that adding a dependency to the constructor no longer breaks the test loudly, it just arrives as a bare mock.
// src/orders/orders.service.spec.ts (variant: useMocker instead of one provider per dependency)
import { beforeEach, describe, it, vi } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { OrdersService } from './orders.service.js';
import { PaymentsService } from '../payments/payments.service.js';
import { ORDERS_REPOSITORY } from './orders.repository.js';
describe('OrdersService', () => {
let service: OrdersService;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [OrdersService], // only the real class is listed
})
.useMocker((token) => {
// The factory runs once per dependency Nest cannot find in this module.
// token is whatever the constructor asked for: a class, a string or a symbol.
if (token === PaymentsService) {
return { charge: vi.fn().mockResolvedValue({ id: 'ch_1' }) };
}
if (token === ORDERS_REPOSITORY) {
return { findById: vi.fn(), save: vi.fn() };
}
// Returning undefined leaves the token unresolved and compile() throws.
return undefined;
})
.compile();
service = moduleRef.get(OrdersService);
});
});Overriding an injected TypeORM repository
Any Nest integration that injects by a generated token has a matching helper you must use in the test: getRepositoryToken(Entity) and getDataSourceToken() from @nestjs/typeorm, getModelToken(name) from @nestjs/mongoose. Guessing the token is the usual cause of 'Nest can't resolve dependencies of UsersService (?)' in a spec that looks correct.
// src/users/users.service.spec.ts
import { beforeEach, expect, it, vi } from 'vitest';
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity.js';
import { UsersService } from './users.service.js';
// UsersService declares:
// constructor(@InjectRepository(User) private readonly users: Repository<User>) {}
// @InjectRepository(User) does not inject under the class Repository. It injects
// under the token getRepositoryToken(User) returns, so that is what the test binds.
const repository = { findOne: vi.fn(), save: vi.fn() };
let service: UsersService;
beforeEach(async () => {
vi.clearAllMocks();
const moduleRef = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: repository },
],
}).compile();
service = moduleRef.get(UsersService);
});
it('looks a user up by email', async () => {
repository.findOne.mockResolvedValue({ id: 1, email: '[email protected]' });
await service.findByEmail('[email protected]');
expect(repository.findOne).toHaveBeenCalledWith({ where: { email: '[email protected]' } });
});A provider that loads data in onModuleInit
This is the shape to copy whenever the class under test implements OnModuleInit, OnApplicationBootstrap or OnModuleDestroy. compile() stops after construction; init() walks the module graph and awaits each hook. Without the init() call the cache is empty and the failure looks like a bug in the service rather than a missing step in the test.
// src/rates/rates.service.spec.ts
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { RatesService } from './rates.service.js';
import { RatesApi } from './rates.api.js';
// RatesService implements OnModuleInit and fills an in-memory cache there:
// async onModuleInit() { this.cache = await this.api.fetchAll(); }
const api = { fetchAll: vi.fn() };
let moduleRef: TestingModule;
let service: RatesService;
beforeEach(async () => {
vi.clearAllMocks();
api.fetchAll.mockResolvedValue({ EUR: 0.92 });
moduleRef = await Test.createTestingModule({
providers: [RatesService, { provide: RatesApi, useValue: api }],
}).compile();
service = moduleRef.get(RatesService);
});
afterEach(async () => {
// Runs onModuleDestroy and onApplicationShutdown on every provider.
await moduleRef.close();
});
it('has no rates before init and the loaded rates after it', async () => {
expect(api.fetchAll).not.toHaveBeenCalled(); // compile() ran the constructor only
await moduleRef.init(); // hooks run here
expect(service.convert(100, 'EUR')).toBe(92);
});
Discussion