Files
What you'll learn
- Build a Nest module inside a test with Test.createTestingModule and compile(), and explain why the hook that does it is async
- Provide a class's dependencies in the test, real or replaced, under the same tokens the application uses
- Retrieve instances from the compiled module with get(), by class and by token
The Testing Module
new CatsService() worked in the last lesson because the service needed nothing. The real one, from Fundamentals, needs a Logger and a configuration under CATS_CONFIG, and its constructor is not the whole story: the logger has its own dependencies, the config is a value some module provides, and @Inject() decides what arrives where. Building that by hand in a test means rebuilding the container by hand, and the first refactor breaks every test that did. Nest's answer is to let a test build a module, the same way the application does, and ask it for instances.
Building a module in a test
@nestjs/testing exports Test, whose createTestingModule() takes exactly what @Module() takes:
import { Test } from '@nestjs/testing';
const moduleRef = await Test.createTestingModule({
providers: [OrdersService, { provide: PaymentGateway, useClass: FakeGateway }],
}).compile();
const orders = moduleRef.get(OrdersService);
compile() builds the container: it resolves every provider, runs the constructors, injects what each asks for, and returns a TestingModule. It is asynchronous, because a module may have async providers and lifecycle hooks, so the beforeEach that calls it is async and awaits it. get(token) then returns an instance, by class or by any token the module knows, exactly like the ModuleRef from Fundamentals. Everything in the module is real Nest: the same injector, the same scopes, the same errors when something is missing.
The test decides the dependencies
The providers list is the test's, not the application's. That is the point. A test provides what makes the class under test observable and predictable:
- the real class, when it is cheap and deterministic:
MemoryLoggeris both, and reading its lines back is a better assertion than checking a mock was called; - a value of the test's choosing under a token:
{ provide: CATS_CONFIG, useValue: { maxCats: 2 } }makes the capacity two for this test, whatever the application configures, so the boundary can be tested with three cats instead of a hundred; - a stand-in for anything slow, external or random, which is the next lesson.
Because the tokens are the application's own, nothing in CatsService knows it is under test. That is the property to keep: a class that needs special hooks for testing is a class whose design is telling you something.
Getting instances back
moduleRef.get(CatsService) returns the singleton the module built; a second get() returns the same one. get(Logger) works for a class used as a token, get(CATS_CONFIG) for a symbol, get('CONNECTION') for a string, each typed as what the provider declares. With beforeEach, every test gets a freshly compiled module and fresh instances, so state cannot leak from one test to the next. Compiling per test costs a few milliseconds here; for a module that connects to something, beforeAll and one compile is the usual trade, with the cleanup in afterAll.
What a testing module can hold
imports, controllers and providers, as a normal module. Importing a real feature module pulls in everything it declares, which is sometimes what you want (an integration test of the module as shipped) and sometimes far too much (a database connection at the bottom of it). Lesson 3 shows overrideProvider(), the tool for importing the real module and swapping the one provider that must not run.
Your task
CatsService is the one from Fundamentals: it needs a Logger and a CatsConfig under CATS_CONFIG, logs every creation, and refuses cats past maxCats.
- In
beforeEach, build a testing module that provides the service,Loggeras aMemoryLogger, and a config of two cats, and take the service and the logger out of it. - Write the three tests: creating a cat logs
created cat #<id>; the third cat is refused withBadRequestException, because the test said two; the cats that fit are all listed.
The bugs to catch are in the service. One of them ignores the configuration and uses three, which only a test that configured something other than three can notice.
When it fails
Nest can't resolve dependencies of the CatsService (?, ...): the testing module lacks a provider the service asks for. It is the same error, from the same injector, as at boot.logger.lines is not a function:get(Logger)returned nothing useful becauseLoggerwas provided as a value without that method, or the variable was never assigned inbeforeEach.- The capacity test passes for the wrong reason: with
maxCats: 3in the test the hard-coded bug is invisible. The test's configuration has to differ from the default to prove the service reads it. compile is not a functionor a Promise where an instance should be:compile()was not awaited, orcreateTestingModule()was not called with an object.
Remember
Test.createTestingModule({ providers, imports, controllers }).compile()builds a real Nest module for the test; await it.- The test chooses the providers: real classes when cheap, values under the application's own tokens when the test needs control.
moduleRef.get(token)returns the built instance, by class, symbol or string.- Fresh module per test with
beforeEach; one per file withbeforeAllwhen building is expensive.
Stuck? Show a hint
In beforeEach: const moduleRef = await Test.createTestingModule({ providers: [CatsService, { provide: Logger, useClass: MemoryLogger }, { provide: CATS_CONFIG, useValue: { maxCats: 2 } }] }).compile(); then service = moduleRef.get(CatsService) and logger = moduleRef.get(Logger). The tests then create cats and check logger.lines(), the third create() with toThrow, and findAll().
Press Run tests to start the app. Its log appears here.Tests
- cats/cats.service.spec.tsrun to see its tests
Bugs your tests must catch
Each of these replaces one file with a buggy version and runs your tests again. A bug is caught when at least one test fails against it. Tests that pass whatever the code does are not tests.
- one cat more than the capacity is acceptedcats/cats.service.ts
- creating a cat logs nothingcats/cats.service.ts
- the capacity is hard-coded to three, the config ignoredcats/cats.service.ts