A unit test for OrdersController calls Test.createTestingModule({...}).compile(), then moduleRef.get(AuditLogService) to grab the mock and assert it was called. Every other provider in the test resolves without a hitch. This one throws:
Error: AuditLogService is a scoped provider. Use "moduleRef.resolve()" instead of "moduleRef.get()".
Nothing about the test setup looks wrong. AuditLogService is in the providers array. It was overridden with useValue. The override even worked — the controller under test is using the mock, not the real thing. The one line that's wrong is the retrieval method, and until you understand what get() and resolve() are each actually reaching into, the fix looks like superstition: "just switch to resolve() and it works," without knowing why.
This is written against NestJS 12.0.x (verified September 2026, current @nestjs/core release). Everything here — Test.createTestingModule, the override methods, get()/resolve() — has been stable across the v9–v12 line; nothing in this article depends on a v12-specific change.
What you'll learn
By the end of this article you'll be able to:
- Explain what a NestJS testing module actually is — not a mock registry, a real DI container
- Use
overrideProvider().useValue()/.useClass()/.useFactory()to swap a dependency without breaking the rest of the module's wiring - Know exactly when
module.get()works and when it throws, and reach formodule.resolve()correctly when it does - Use
useMockerto stop hand-writing boilerplate mocks — and know when not to - Override a guard, pipe, interceptor, or filter for an end-to-end test, not just a constructor-injected provider
Who this is for
You've written a NestJS service or two, used @Injectable() and constructor injection, and you've run npm test on a generated Nest project at least once. If you haven't yet read the NestJS dependency injection episode of this series, the scope material here (DEFAULT vs REQUEST vs TRANSIENT) will make more sense with that as background — but this article stands on its own.
Table of contents
- The problem: mocking a NestJS service the naive way
- The mental model: the testing module is a real container
- Stage 1: overrideProvider, the smallest correct example
- Stage 2: useFactory, when the mock needs its own dependencies
- Stage 3: useMocker, auto-mocking the rest of the graph
- Stage 4: get() vs resolve(), the scoped-provider gotcha
- Stage 5: overriding guards, pipes, interceptors, and filters
- Edge cases and gotchas
- Best practices
- FAQ
- Cheat sheet
- Key takeaways
The problem: mocking a NestJS service the naive way
Say OrdersService depends on CacheService:
@Injectable()
export class OrdersService {
constructor(private readonly cache: CacheService) {}
async getOrder(id: string) {
const cached = await this.cache.get(id);
if (cached) return cached;
// ...load from DB, then this.cache.set(id, order)
}
}
The instinct, coming from plain Node testing, is to skip Nest entirely:
// The wrong way — bypasses Nest's DI graph completely
const fakeCache = { get: jest.fn(), set: jest.fn() };
const service = new OrdersService(fakeCache as any);
This "works" for exactly this constructor. It breaks the moment someone adds a second constructor parameter — every call site of new OrdersService(...) in every test file needs updating by hand, because you're testing against the implementation of the constructor, not the contract Nest resolves it through. It also can't test anything that depends on Nest actually wiring the object graph: a guard reading a provider, a module boundary, an onModuleInit hook. You've tested a plain JavaScript class, not a NestJS provider.
The second instinct is jest.mock('./cache.service'), patching the module import. This fights Nest's instantiation instead of cooperating with it — it works until the service is constructed through useFactory or aliased with useExisting, at which point the mock and the real DI graph disagree about what CacheService even resolves to, and the failure mode is confusing rather than obvious.
Nest ships a purpose-built answer to both problems: @nestjs/testing.
The mental model: the testing module is a real container
The mental model: Test.createTestingModule({...}) doesn't create a fake, lightweight stand-in for your app — it builds the exact same kind of DI container Nest builds when your app boots, from the same module metadata. overrideProvider(Token) doesn't monkey-patch anything after the fact; it swaps what Token resolves to before .compile() runs, so when the container is built, every real consumer that constructor-injects that token gets your replacement — automatically, through the same resolution path as production.
That's the whole idea in one sentence: override the registration, then let the real container do the wiring. You never hand-assemble the object graph yourself, so a test can't quietly drift from how the app is actually wired.
const moduleRef = await Test.createTestingModule({
providers: [OrdersService, CacheService],
})
.overrideProvider(CacheService)
.useValue(fakeCache)
.compile();
const service = moduleRef.get(OrdersService);
// service.cache is `fakeCache` — Nest injected it, you didn't.
Key concept: OrdersService never mentions the mock. It still says constructor(private readonly cache: CacheService). The override happens one layer up, in how the token is registered — which is exactly why it survives constructor changes that don't touch CacheService's shape.
Stage 1: overrideProvider, the smallest correct example
The three replacement methods mirror the ones you already know from provider registration itself:
Test.createTestingModule({ providers: [OrdersService, CacheService] })
.overrideProvider(CacheService)
.useValue(fakeCache) // a plain object/mock — no DI, no lifecycle
.compile();
Test.createTestingModule({ providers: [OrdersService, CacheService] })
.overrideProvider(CacheService)
.useClass(InMemoryCacheService) // a real class, its own constructor DI-resolved
.compile();
useValue is the right default: fast, explicit, and you can inspect fakeCache.get.mock.calls directly. Reach for useClass when the fake needs real behavior (an in-memory cache that actually stores and evicts), because unlike useValue, useClass still goes through Nest's own instantiation — its constructor can inject other test doubles too.
Stage 2: useFactory, when the mock needs its own dependencies
Sometimes the fake itself needs something resolved from the container — a config value, another mock, the module ref:
Test.createTestingModule({ providers: [OrdersService, CacheService, ConfigService] })
.overrideProvider(CacheService)
.useFactory({
factory: (config: ConfigService) => new FakeCacheWithTtl(config.get('CACHE_TTL')),
inject: [ConfigService],
})
.compile();
Key concept: inject is resolved from the same container being built — including other overrides. This is the one case where the mock still participates in DI, rather than being a static value dropped in from outside.
Stage 3: useMocker, auto-mocking the rest of the graph
A module with six providers and you only care about testing one of them means five hand-written useValue mocks that add nothing to the assertions. useMocker removes that boilerplate: pass it a factory, and Nest calls it for every provider you didn't explicitly override, generating a jest auto-mock (every method replaced with jest.fn()) by default:
const moduleRef = await Test.createTestingModule({
providers: [OrdersService, CacheService, AuditLogService, MetricsService],
})
.useMocker((token) => {
if (token === CacheService) return fakeCache; // your real, meaningful mock
if (typeof token === 'function') return jest.fn(); // auto-mock the rest
})
.compile();
This is genuinely useful for modules with a lot of incidental dependencies. It is also easy to overuse: if useMocker quietly auto-mocks the one provider your test is actually supposed to exercise, the test passes for the wrong reason — it never called anything real. Use it to remove noise around the thing you're testing, never to remove the thing you're testing.
Stage 4: get() vs resolve(), the scoped-provider gotcha
This is the error from the opening of this article, and it isn't a bug — it's Scope doing exactly what it's documented to do.
module.get(Token) reaches into the container's static registrations — the ones built once at .compile() time. That's every DEFAULT-scoped (singleton) provider. module.resolve(Token) is asynchronous, and creates (or looks up) a request-scoped sub-tree — which is the only kind of container a REQUEST- or TRANSIENT-scoped provider can live in.
@Injectable({ scope: Scope.REQUEST })
export class AuditLogService { /* ... */ }
// Throws — a REQUEST-scoped provider has no single static instance to hand back
moduleRef.get(AuditLogService);
// Correct — resolves a fresh instance for this call
const auditLog = await moduleRef.resolve(AuditLogService);
Two details make this behave differently from get() in ways that trip people up:
-
It's a Promise. Forgetting
awaitgives you aPromise<AuditLogService>that happily passes typechecking in a loosely-typed test and fails at runtime in a confusing spot. -
Every call gets a new instance, by default. Two
moduleRef.resolve(AuditLogService)calls in the same test return two different objects — because in production, two different HTTP requests would too. If you need the same instance twice (to simulate one request touching the provider from two places), pass the same context id explicitly:ContextIdFactory.create()once, thenmoduleRef.resolve(Token, contextId)for each call that should share it.
Key concept: get() vs resolve() isn't a style choice — it's Scope.DEFAULT vs everything else, the exact distinction from how NestJS resolves providers in production. A test that gets this right is also proof the provider's scope is doing what you think it's doing.
Stage 5: overriding guards, pipes, interceptors, and filters
Everything so far overrides a provider — something resolved through constructor injection. Guards, pipes, interceptors, and filters applied with @UseGuards(), @UsePipes(), @UseInterceptors(), and @UseFilters() (or registered globally) are a separate concern in Nest's request pipeline, so TestingModuleBuilder ships parallel methods for them, with the identical useValue/useClass/useFactory chain:
const moduleRef = await Test.createTestingModule({
controllers: [OrdersController],
})
.overrideGuard(RolesGuard)
.useValue({ canActivate: () => true }) // always allow, for this test
.compile();
const app = moduleRef.createNestApplication();
await app.init();
// now use supertest against `app.getHttpServer()` for a real e2e request
This is the tool for an end-to-end test — one that goes through createNestApplication() and an actual HTTP request via supertest, exercising the real request lifecycle (middleware → guards → interceptors → pipes → handler → interceptors → filters, covered in the request lifecycle episode of this series) with just the one guard swapped out. It is the difference between testing "does OrdersService.getOrder() return the right shape" and "does a real request to GET /orders/:id actually reach the handler."
Edge cases and gotchas
-
Overrides must be declared before
.compile(). There is no "override after the fact" — the testing module is immutable once built, same as the real app's container. -
Overriding module boundaries doesn't undo
exports. IfCacheServiceisn't exported from the module that owns it, importing that module into your test setup still won't expose it — the same encapsulation rules from a real app apply inside a test. -
{ strict: false }onget()widens the search, not the scope.moduleRef.get(Token, { strict: false })lets you fetch a provider that lives in a different module in the graph than the one you passed tocreateTestingModule— it does not letget()reach a scoped provider. That still needsresolve(). -
Lifecycle hooks still run.
onModuleInit,onApplicationBootstrap, and friends fire for real during.compile()(andapp.init()for a full app) exactly as they would in production. A provider that opens a real connection inonModuleInitwill try to open one in your test too, unless it's the thing you overrode. -
overrideModuleswaps a whole module, not a token. It's the heaviest override available — useful for replacing an entireDatabaseModulewith an in-memory test double — and correspondingly rare; reach foroverrideProviderfirst.
Best practices
-
Override the narrowest token that makes the test honest. Swap
CacheService, notOrdersModule— the more of the real graph you keep, the more the test proves. -
Prefer real modules plus
overrideProviderover hand-built instances. Let Nest do the constructor wiring; don't reimplement it in a test helper that can drift from the real module. -
Reach for
useMockerto cut noise, not to cut assertions. If a test can pass without the "real" mock ever being asked to do anything, that mock probably shouldn't be auto-generated. -
Match the real provider's scope in the fake. A
useValuemock standing in for aREQUEST-scoped provider is fine for a unit test that doesn't care about per-request identity — but if the article'sresolve()distinction matters to what you're testing, keep the scope on the override too. -
Use
createNestApplication()sparingly. It's the right tool for a genuine end-to-end test of routing, guards, and the lifecycle — not the default for testing one service's logic.
FAQ
Why does moduleRef.get() throw for a provider I know is registered?
Because it's DEFAULT-scoped only. get() reads the container's static registrations, built once at .compile(). A REQUEST- or TRANSIENT-scoped provider has no single static instance — use moduleRef.resolve() instead, and await it.
Can I call overrideProvider() after compile()?
No. Every override method has to be chained onto the builder before .compile() runs; the compiled TestingModule is a finished container.
Is overrideProvider the same as jest.mock()?
No, and that's the point. jest.mock() patches a module import at the file-system/require level, outside Nest's awareness. overrideProvider() changes what a token resolves to inside Nest's own DI graph, so every real consumer that constructor-injects it gets the override through the identical resolution path production uses.
Does useMocker replace the need for overrideProvider?
No — they compose. Use explicit overrideProvider().useValue() for the providers your assertions actually check, and let useMocker fill in everything else so you're not hand-writing mocks for providers the test doesn't care about.
Do I need createNestApplication() for a unit test?
No. moduleRef.get()/.resolve() against the compiled TestingModule is enough for testing a single provider's logic. createNestApplication() is for when the test needs the real HTTP request lifecycle — routing, guards, pipes, filters — not just a provider's method.
🎮 Try it yourself
▶️ Open the interactive playground →
Runs right in your browser — poke at it and watch the concept react live.
Cheat sheet
| Task | Code | Notes |
|---|---|---|
| Build a test module | await Test.createTestingModule({ providers, imports, controllers }).compile() |
Same DI graph shape as a real app module |
| Replace a value/mock | .overrideProvider(Token).useValue(mock) |
No DI on the mock itself |
| Replace with a real fake class | .overrideProvider(Token).useClass(FakeImpl) |
FakeImpl's own constructor is still DI-resolved |
| Replace with a computed value | .overrideProvider(Token).useFactory({ factory, inject }) |
inject resolves from the same container |
| Auto-mock everything else | .useMocker((token) => ...) |
Return undefined to let Nest auto-mock; explicit mocks for what you assert on |
Get a DEFAULT-scoped provider |
moduleRef.get(Token) |
Throws for REQUEST/TRANSIENT scope |
| Get a scoped provider | await moduleRef.resolve(Token) |
Async; new instance per call unless given the same contextId
|
| Search outside the local module | moduleRef.get(Token, { strict: false }) |
Widens search, not scope — still can't fetch a scoped provider |
| Override a guard/pipe/interceptor/filter | .overrideGuard(G).useValue({ canActivate: () => true }) |
Same chain as overrideProvider; needs createNestApplication() for e2e |
| Swap an entire module | .overrideModule(RealModule).useModule(FakeModule) |
Heaviest option — use rarely |
🧠 Test yourself
Think it clicked? Take the 10-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
Key takeaways
-
Test.createTestingModulecompiles a real DI container from the same module metadata your app boots with — it isn't a mock registry. -
overrideProviderswaps a token's registration before compilation, so every real consumer still gets wired to it through normal constructor injection. -
module.get()only reachesDEFAULT-scoped providers;REQUESTandTRANSIENTscope requireawait module.resolve()— a different method, not an option flag. -
useMockerauto-generates mocks for providers you didn't explicitly override — a time-saver that can also hide a test that never exercises anything real. - Guards, pipes, interceptors, and filters get their own override methods, aimed at end-to-end tests through
createNestApplication(), not at constructor-injected providers.
That confusing "use resolve() instead of get()" error from the opening isn't NestJS being finicky — it's the same Scope contract from the DI container showing up at test time instead of in production, which is exactly where you'd rather find it. Now when a test throws that error, you'll know precisely which of the two questions it's actually asking: is this provider a singleton, or isn't it?
What's the trickiest NestJS testing bug you've run into — a scope mismatch, a missing override, something else entirely? Drop it in the comments.
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___