Unit Testing Providers
Unit testing a provider means exercising one class — usually a service — with every collaborator it depends on replaced by a controllable fake. NestJS makes this straightforward because providers are wired through dependency injection, so any constructor dependency can be swapped for a mock. The @nestjs/testing package gives you Test.createTestingModule, a lightweight version of @Module() that builds a real DI container around the class under test while letting you decide exactly what gets injected. The result is fast, deterministic tests that never touch a database, network, or filesystem.
The service under test
Consider a CatsService that depends on a repository. In a unit test we do not want the real repository — we only want to verify the service’s own logic and that it calls its collaborators correctly.
import { Injectable } from '@nestjs/common';
import { CatsRepository } from './cats.repository';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
constructor(private readonly repo: CatsRepository) {}
async findOne(id: number): Promise<Cat> {
const cat = await this.repo.findById(id);
if (!cat) {
throw new Error(`Cat ${id} not found`);
}
return cat;
}
async countAdults(): Promise<number> {
const cats = await this.repo.findAll();
return cats.filter((c) => c.age >= 2).length;
}
}
Building a testing module
A unit test registers the real provider you want to test and supplies a mock for each of its dependencies using the useValue custom-provider syntax. Calling .compile() resolves the graph and returns a TestingModule, from which you pull instances with module.get().
import { Test, TestingModule } from '@nestjs/testing';
import { CatsService } from './cats.service';
import { CatsRepository } from './cats.repository';
describe('CatsService', () => {
let service: CatsService;
let repo: jest.Mocked<CatsRepository>;
beforeEach(async () => {
const repoMock: Partial<jest.Mocked<CatsRepository>> = {
findById: jest.fn(),
findAll: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
CatsService,
{ provide: CatsRepository, useValue: repoMock },
],
}).compile();
service = module.get<CatsService>(CatsService);
repo = module.get(CatsRepository);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
Because CatsRepository is provided with useValue, Nest injects the mock object instead of constructing the real class. The jest.Mocked<T> type preserves the original signatures while typing each method as a Jest mock, so autocomplete and the compiler still protect you.
Asserting behavior
With the module compiled you call the service methods directly and assert on both the return value and how the mock was used. Program the mock’s return value with mockResolvedValue (for promises) before invoking the method.
it('returns the cat when it exists', async () => {
repo.findById.mockResolvedValue({ id: 1, name: 'Felix', age: 3 });
const cat = await service.findOne(1);
expect(cat.name).toBe('Felix');
expect(repo.findById).toHaveBeenCalledWith(1);
expect(repo.findById).toHaveBeenCalledTimes(1);
});
it('throws when the cat is missing', async () => {
repo.findById.mockResolvedValue(undefined);
await expect(service.findOne(99)).rejects.toThrow('Cat 99 not found');
});
it('counts only adult cats', async () => {
repo.findAll.mockResolvedValue([
{ id: 1, name: 'Felix', age: 3 },
{ id: 2, name: 'Milo', age: 1 },
{ id: 3, name: 'Nala', age: 5 },
]);
expect(await service.countAdults()).toBe(2);
});
Running the suite gives the familiar Jest report:
Output:
PASS src/cats/cats.service.spec.ts
CatsService
✓ should be defined (3 ms)
✓ returns the cat when it exists (2 ms)
✓ throws when the cat is missing (4 ms)
✓ counts only adult cats (1 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Time: 1.482 s
Ways to supply a mock
Test.createTestingModule accepts the same custom-provider forms as a real module, so you can choose the style that fits the dependency.
| Approach | Syntax | When to use |
|---|---|---|
| Static value | { provide: Repo, useValue: mock } | Plain object of jest.fn() stubs |
| Factory | { provide: Repo, useFactory: () => mock } | Mock needs construction logic |
| Override after build | .overrideProvider(Repo).useValue(mock) | Reuse an existing module definition |
| Auto-mock | .useMocker(() => ...) | Mock every unspecified dependency |
The overrideProvider API is useful when importing a feature module wholesale and replacing just one provider:
const module = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsRepository)
.useValue(repoMock)
.compile();
Tip: keep
jest.clearAllMocks()in a globalafterEach(or setclearMocks: truein your Jest config) so call counts and return values do not leak between tests.
Warning: a unit test should never reach a real database or HTTP endpoint. If a test is slow or flaky, an unmocked dependency is usually the cause — verify every constructor argument is provided as a mock.
Best practices
- Provide a mock for every constructor dependency; an unmocked one signals the test is really an integration test.
- Type mocks with
jest.Mocked<T>so renamed or removed methods break compilation rather than passing silently. - Assert on collaborator interactions (
toHaveBeenCalledWith) as well as return values to catch wiring bugs. - Reset mock state between tests with
clearMocksto keep cases independent and order-insensitive. - Prefer
useValuefor simple stubs and reserveoverrideProviderfor when you must import a full module. - Keep one
describeblock per provider and group related cases, so failures point directly at the responsible unit.