Mocking Providers and Time

Swap any token in the container with overrideProvider and friends, and make a time-dependent test deterministic.

Syntax.overrideProvider(TOKEN).useValue(double) | vi.useFakeTimers({ now: new Date('2026-01-01') })

An override replaces one entry in the dependency-injection container. The builder records the replacement, and compile() applies it before any instance is constructed — so every class that injects that token receives the replacement, however deep in the module graph it sits. You do not have to find the module that declared it.

The override methods

.overrideProvider(token) returns an object with three endings: .useValue(obj), .useClass(SomeClass) and .useFactory({ factory, inject }). They mirror the custom-provider forms, so .overrideProvider(X).useValue(v) and { provide: X, useValue: v } are the same binding written two ways. .overrideGuard(), .overrideInterceptor(), .overridePipe() and .overrideFilter() do the same for things bound with @UseGuards and friends, and .overrideModule(Mod).useModule(FakeMod) swaps a whole module. All of them return the builder, so they chain before .compile().

Time

Two approaches, and they are not equivalent. Inject the clock: declare a CLOCK provider whose value exposes now(), and bind a fixed one in the test. Nothing global changes, and the test reads clearly. Or fake the global clock with the runner: vi.useFakeTimers() replaces Date, setTimeout and setInterval; vi.setSystemTime(date) moves the clock without firing timers; vi.advanceTimersByTimeAsync(ms) runs the timers due in that window and awaits what they start. Always restore with vi.useRealTimers().

vi.useFakeTimers({ now: date }) is the short form of calling useFakeTimers() and then setSystemTime(date).

Example

Example · typescript
// src/common/clock.ts
export const CLOCK = Symbol('CLOCK');

export interface Clock {
  now(): Date;
}

export const systemClock: Clock = { now: () => new Date() };

// src/trials/trials.module.ts
import { Module } from '@nestjs/common';
import { CLOCK, systemClock } from '../common/clock.js';
import { TrialsService } from './trials.service.js';

@Module({
  // The real binding. A test replaces this one token and nothing else.
  providers: [TrialsService, { provide: CLOCK, useValue: systemClock }],
  exports: [TrialsService],
})
export class TrialsModule {}

// src/trials/trials.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { CLOCK, type Clock } from '../common/clock.js';

@Injectable()
export class TrialsService {
  // Parameter-property shorthand. The longhand is:
  //   private readonly clock: Clock;
  //   constructor(@Inject(CLOCK) clock: Clock) { this.clock = clock; }
  constructor(@Inject(CLOCK) private readonly clock: Clock) {}

  isExpired(startedAt: Date, days = 14): boolean {
    const deadline = new Date(startedAt);
    deadline.setDate(deadline.getDate() + days);
    return this.clock.now() >= deadline;
  }
}

// src/trials/trials.service.spec.ts
import { beforeEach, describe, expect, it } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { CLOCK, type Clock } from '../common/clock.js';
import { TrialsModule } from './trials.module.js';
import { TrialsService } from './trials.service.js';

describe('TrialsService', () => {
  let service: TrialsService;
  let today: Date;

  beforeEach(async () => {
    today = new Date('2026-03-01T12:00:00.000Z');
    const fixedClock: Clock = { now: () => today };

    const moduleRef: TestingModule = await Test.createTestingModule({
      imports: [TrialsModule], // CLOCK is declared inside this module
    })
      // overrideProvider reaches into the imported module and replaces the token
      // there. Adding { provide: CLOCK, ... } at the top level would not.
      .overrideProvider(CLOCK)
      .useValue(fixedClock)
      .compile();

    service = moduleRef.get(TrialsService);
  });

  it('expires a trial started fifteen days ago', () => {
    expect(service.isExpired(new Date('2026-02-14T12:00:00.000Z'))).toBe(true);
  });

  it('does not expire a trial started yesterday', () => {
    expect(service.isExpired(new Date('2026-02-28T12:00:00.000Z'))).toBe(false);
  });
});

When to use it

  • Freeze the clock so a trial-expiry or token-expiry test gives the same answer every day.
  • Replace an auth guard with one that always allows, in a suite about something else.
  • Stop an end-to-end run from sending real email or SMS while keeping the rest of the app real.
  • Advance a retry backoff by six seconds instantly instead of sleeping through it.

More examples

Overriding guards, interceptors, pipes and filters

Use these four methods when a pipeline component is a dependency of the test rather than its subject: an auth guard in a suite about ordering logic, an interceptor that ships metrics, a filter that posts to an error tracker. The overrides are keyed by class, and they only match components Nest instantiated - a guard registered as an already-constructed instance, app.useGlobalGuards(new RolesGuard()), is not in the container and cannot be overridden.

Example · typescript
// test/admin.e2e-spec.ts
import { beforeAll, afterAll, it } from 'vitest';
import request from 'supertest';
import { Test } from '@nestjs/testing';
import {
  CallHandler,
  CanActivate,
  ExecutionContext,
  INestApplication,
  NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { AppModule } from '../src/app.module.js';
import { RolesGuard } from '../src/auth/roles.guard.js';
import { AuditInterceptor } from '../src/common/audit.interceptor.js';

let app: INestApplication;

// A guard decides whether the handler runs at all; it returns a boolean.
const allowAll: CanActivate = { canActivate: () => true };

// An interceptor wraps the handler: code before, then next.handle(), then the
// operators applied to the stream it returns. This one just gets out of the way.
const passThrough: NestInterceptor = {
  intercept: (_ctx: ExecutionContext, next: CallHandler): Observable<unknown> =>
    next.handle(),
};

beforeAll(async () => {
  const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
    .overrideGuard(RolesGuard)
    .useValue(allowAll)
    .overrideInterceptor(AuditInterceptor)
    .useValue(passThrough) // the real one writes to an audit table
    .compile();

  app = moduleRef.createNestApplication();
  await app.init();
});

afterAll(async () => {
  await app.close();
});

it('reaches the admin handler', async () => {
  await request(app.getHttpServer()).get('/admin/stats').expect(200);
});

Advancing a retry without waiting for it

This is the pattern for any delay the code imposes on itself: retry backoff, debounce, a cache TTL, a token refreshed shortly before expiry. Reach for advanceTimersByTimeAsync rather than the synchronous form whenever the timer callback awaits something. Note that a @Cron handler needs none of this - it is an ordinary method, so call it directly and leave the schedule string to a separate check.

Example · typescript
// src/webhooks/webhooks.service.spec.ts
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { Test } from '@nestjs/testing';
import { WebhooksService } from './webhooks.service.js';
import { HttpSender } from './http-sender.js';

// WebhooksService retries a failed delivery after 1s, then 5s, then gives up,
// using setTimeout. A real test would otherwise take six seconds.
const sender = { post: vi.fn() };
let service: WebhooksService;

beforeEach(async () => {
  vi.clearAllMocks();

  const moduleRef = await Test.createTestingModule({
    providers: [WebhooksService, { provide: HttpSender, useValue: sender }],
  }).compile();

  service = moduleRef.get(WebhooksService);
});

afterEach(() => {
  vi.useRealTimers(); // never leave fake timers installed for the next test
});

it('retries twice and then succeeds', async () => {
  sender.post
    .mockRejectedValueOnce(new Error('503'))
    .mockRejectedValueOnce(new Error('503'))
    .mockResolvedValueOnce({ status: 200 });

  vi.useFakeTimers(); // install them here, around the code under test only

  const delivery = service.deliver({ url: 'https://example.test/hook', body: {} });

  // The async variant runs the timers due in the window and awaits the promises
  // their callbacks start. advanceTimersByTime (sync) would not.
  await vi.advanceTimersByTimeAsync(1_000);
  await vi.advanceTimersByTimeAsync(5_000);

  await expect(delivery).resolves.toMatchObject({ status: 200 });
  expect(sender.post).toHaveBeenCalledTimes(3);
});

A factory override and a module override

useFactory is the override to use when the double itself needs configuration - a region, a base URL, a feature flag - because inject gives it the real ConfigService. overrideModule is the blunter tool: prefer overriding the one provider you care about, and swap the whole module only when it registers something awkward at import time, such as a client that opens a connection in its own factory.

Example · typescript
// test/notifications.e2e-spec.ts
import { beforeAll, afterAll, expect, it, vi } from 'vitest';
import { Test } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { INestApplication, Module } from '@nestjs/common';
import { AppModule } from '../src/app.module.js';
import { MailModule } from '../src/mail/mail.module.js';
import { MailService } from '../src/mail/mail.service.js';
import { SMS_CLIENT } from '../src/sms/sms.tokens.js';

// A drop-in replacement module. It must export the same tokens the real one
// exports, or the classes importing MailModule stop resolving.
const sent: Array<{ to: string }> = [];

@Module({
  providers: [
    {
      provide: MailService,
      useValue: { send: async (to: string) => void sent.push({ to }) },
    },
  ],
  exports: [MailService],
})
class FakeMailModule {}

let app: INestApplication;

beforeAll(async () => {
  const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
    // useModule replaces the module wherever AppModule imports it, including
    // transitively through a feature module.
    .overrideModule(MailModule)
    .useModule(FakeMailModule)
    // useFactory takes the same { factory, inject } shape as a custom provider:
    // inject lists tokens, factory receives their instances in that order.
    .overrideProvider(SMS_CLIENT)
    .useFactory({
      inject: [ConfigService],
      factory: (config: ConfigService) => ({
        from: config.get<string>('SMS_FROM'),
        send: vi.fn().mockResolvedValue({ sid: 'SM_test' }),
      }),
    })
    .compile();

  app = moduleRef.createNestApplication();
  await app.init();
});

afterAll(async () => {
  await app.close();
});

it('records the welcome mail instead of sending it', async () => {
  expect(sent).toHaveLength(0);
});

Discussion

  • Be the first to comment on this lesson.