DEV Community

Maximiliano Ezequiel Carrizo
Maximiliano Ezequiel Carrizo

Posted on

Decorator-based Nest.js 12 tests with Jest in ESM mode

Nest.js 12 packages are ESM only, and Jest can only load them in its native ESM mode. So a Nest.js 12 test suite on Jest needs some setup before it runs at all.

nestjs-jest-decorators works in that setup, and it also lets you write tests as classes. Suites and tests are plain classes and methods, and test classes are Nest providers, so you use @Inject() instead of app.get(Component).

@TestSuite('Demo Test Suite')
export class DemoTest {
  @Inject()
  private readonly httpService!: HttpService;

  @Test('Get Test')
  async testGet(): Promise<void> {
    const { data } = await firstValueFrom(this.httpService.get('http://localhost:4343/demo'));
    expect(data).toBe('Hello, Test world !');
  }
}
Enter fullscreen mode Exit fullscreen mode

@TestSuite is a Jest describe and @Test is a Jest it.

Installation

You need Nest.js 12, Jest 30 and Node.js ^20.19.0, ^22.12.0 or >=24.0.0. To use ts-jest, stay on TypeScript 6.0 or lower.

npm i --save-dev nestjs-jest-decorators jest @jest/globals ts-jest typescript@~6.0.0 @nestjs/testing @types/node
Enter fullscreen mode Exit fullscreen mode

Your tsconfig.json needs these options:

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler",
    "types": ["node"],
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Turn on Jest's ESM mode

Add jest.config.mjs:

export default {
  testEnvironment: 'node',
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' },
  transform: { '^.+\\.ts$': ['ts-jest', { useESM: true }] },
  testMatch: ['<rootDir>/test/test.entrypoint.ts'],
  verbose: true,
};
Enter fullscreen mode Exit fullscreen mode

Then add the test script to your package.json:

{
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

ESM mode is still experimental in Jest, so it needs the --experimental-vm-modules flag, and Node.js prints an ExperimentalWarning when it runs.

Boot the app and declare the tests

Create test/test.entrypoint.ts:

import { afterAll, beforeAll } from '@jest/globals';
import { HttpModule } from '@nestjs/axios';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { declareTests, JestTestModule } from 'nestjs-jest-decorators';
import { DemoTest } from './tests/demo.test.js';
import { YourAppModule } from './your-app/your-app.module.js';

const tests = [DemoTest];

let app: INestApplication;

beforeAll(async () => {
  const testingModule = await Test.createTestingModule({
    imports: [
      YourAppModule, // The app to test
      JestTestModule.registerTests(tests, [HttpModule]), // Your tests, and the modules they need
    ],
  }).compile();

  app = testingModule.createNestApplication();
  await app.listen(4343);
});

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

declareTests(tests, () => app);
Enter fullscreen mode Exit fullscreen mode

Jest collects tests synchronously, before any beforeAll runs, and it has no way to delay that. That's why declareTests receives a function: tests are declared right away, and the app is only asked for each instance when a test runs.

Share tests with inheritance

Tests declared in a base class run in each subclass decorated with @TestSuite, and a subclass can override a test method:

abstract class CrudTest {
  @Test('Lists items')
  async list(): Promise<void> {
    // ...
  }
}

@TestSuite('Users')
export class UsersTest extends CrudTest {}

@TestSuite('Orders')
export class OrdersTest extends CrudTest {}
Enter fullscreen mode Exit fullscreen mode

Run code around every test

Create a class with @TestEventsSuite() and add it to your list of tests. Its hooks run around every test of every suite:

import { TestEvent, TestEventsEnum, TestEventsSuite } from 'nestjs-jest-decorators';

@TestEventsSuite()
export class Events {
  @TestEvent(TestEventsEnum.BEFORE_EACH)
  async beforeEach(): Promise<void> {
    // Runs before each test
  }

  @TestEvent(TestEventsEnum.AFTER_EACH)
  async afterEach(): Promise<void> {
    // Runs after each test
  }
}
Enter fullscreen mode Exit fullscreen mode

Run a single suite or test

Pick one with an environment variable. Names must match exactly, and Jest fails if no test matches, so a typo won't look like a successful run:

SUITETORUN="Demo Test Suite" npm test
TESTTORUN="Get Test" npm test
Enter fullscreen mode Exit fullscreen mode

Try it

The library is MIT licensed. You can find it on npm and GitHub, where issues and suggestions are welcome.

Top comments (0)