InteractiveFrameworks

The Testing Module

Build a class with its dependencies inside a test with Test.createTestingModule, provide the test's own values under its tokens, and take instances back out with get().

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

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: MemoryLogger is 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.

  1. In beforeEach, build a testing module that provides the service, Logger as a MemoryLogger, and a config of two cats, and take the service and the logger out of it.
  2. Write the three tests: creating a cat logs created cat #<id>; the third cat is refused with BadRequestException, 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 because Logger was provided as a value without that method, or the variable was never assigned in beforeEach.
  • The capacity test passes for the wrong reason: with maxCats: 3 in 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 function or a Promise where an instance should be: compile() was not awaited, or createTestingModule() 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 with beforeAll when 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().