Nestjs integration tests learnings

Venez en guilde CI !

Quick context

The technos

superTest (superAgent)

The story

The story

The story

The story

The tests were exceeding 8 minutes 

The architecture

The Improvements

State of the art

  • Parallelise tests
  • Launch tests with ts-jest

Tests parallelisation

The problem

Most of our tests are integration tests, they use the databases. 

Prefix our entities / collections with the id of the jest worker.

The solution

Tests parallelisation

Concretely

// setup.ts

process.env.TYPEORM_ENTITY_PREFIX = process.env.JEST_WORKER_ID;
process.env.MONGO_SCHEMA_PREFIX = process.env.JEST_WORKER_ID;

// jest.config.js
module.exports = {
  // ...
  setupFiles: ['<rootDir>/setup.ts']
}

// schema.decorator.ts

import { Schema as DefaultSchema } from '@nestjs/mongoose';
import { SchemaOptions } from 'mongoose';

export const Schema = (options: SchemaOptions): ClassDecorator =>
  DefaultSchema({
    ...options,
    ...(options.collection !== undefined
      ? { collection: (process.env.MONGO_SCHEMA_PREFIX ?? '') + options.collection }
      : {}),
  });

Tests parallelisation

Gain

Tests parallelisation

Nice but...

Tests parallelisation

What is happening ?

Tests parallelisation

Tests parallelisation

The fix

Factor transpilation and run Jest on js code

Tests parallelisation

The fix

Factor transpilation and run Jest on js code

// jest.config.js

module.exports = {
  testEnvironment: 'node',
  rootDir: 'dist',
  collectCoverageFrom: [
    '<rootDir>/src/**/*.js',
    '!<rootDir>/src/main.js',
    '!<rootDir>/src/modules/logger/**',
    '!<rootDir>/src/**/*.d.ts',
  ],
  setupFiles: ['<rootDir>/tests/setup.js']
};

// tsconfig.json

{
  // no tests here
  "exclude": ["node_modules", "dist", "dangerfile.ts", "dangerRules"]
}

Tests parallelisation

What does it change ?

  • Backend must be running in local if you want to run your tests
  • We need to transpile our code before we run the tests in the CI
  • Stack trace in JS in tests (fixable)
  • Minor changes in coverage measure

Tests parallelisation

Gain

Local

CI

After some time without looking

New Ideas

Are tests faster in local just because the machine is more powerful ?

Saving jest cache

// jest.config.js

module.exports = {
  // ...
  cacheDirectory: '<rootDir>/../.jest-cache'
};

// CI.yaml

name: CI

jobs:
  ci:

    steps:
      // checkout code, install dependencies, ...
      
      - uses: actions/cache@v2
        with:
          path: .jest-cache
          key: ${{ runner.os }}-jest-cache-${{ hashFiles('**/*.test.ts') }}
          restore-keys: |
            ${{ runner.os }}-jest-cache-${{ hashFiles('**/*.test.ts') }}
            ${{ runner.os }}-jest-cache-

      - run: yarn test:coverage
      
      // ...

Saving jest cache

New Ideas

Can we optimize the flow of our CI ?

The flow

Install dependencies

Build
code

Run
Tests

// package.json

{
  // ...
  "scripts": {
    // ...
    "start-db": "docker-compose -f docker-compose.test.yml up -d",
    "test": "yarn start-db && dotenv -e .env.test -- node --expose-gc --max_old_space_size=1024 ./node_modules/.bin/jest --maxWorkers=4 --logHeapUsage",
    "test:coverage": "yarn test --coverage --reporters=default --watchAll=false --testResultsProcessor jest-sonar-reporter",
  },
}

Pull docker images

Start dockers

Run
tests

Pulling images earlier

Install dependencies

Build
code

Run
Tests

Find docker images

Start dockers

Run
tests

Pull docker images (detached)

Pulling images earlier

~30s

New Ideas

Can we optimize our tests instead of the tools ?

Reducing the number of user creation / deletion

beforeEach(async () => {
  await createAndSaveUser(userModel, {
    TUWIS: [well.tuwi],
  });
});

afterEach(async () => {
  await userModel.deleteMany({});
});

In each test file, grouping tests that need a user to create the user only once per group

The idea

Before

beforeEach(async () => {
  await createAndSaveUser(userModel, {
    TUWIS: [well.tuwi],
  });
});

afterEach(async () => {
  await userModel.deleteMany({});
});

Reducing the number of user creation / deletion

beforeEach(async () => {
  await createAndSaveUser(userModel, {
    TUWIS: [well.tuwi],
  });
});

afterEach(async () => {
  await userModel.deleteMany({});
});

After

describe('without any authenticated user', () => {
  // tests
});

describe('with an authenticated user', () => {
  beforeAll(async () => {
    await createAndSaveUser(userModel, {
      TUWIS: [well.tuwi],
    });
  });
   afterAll(async () => {
    await userModel.deleteMany({});
  });
  
  // tests
});

Reducing the number of user creation / deletion

What's next ?

Reducing the number of creations for other entities (eg: wells)

Move a part of our tests to unit tests

We were mostly doing integration tests because our services require a lot of injected stuf. And the things we needed to inject are hard to mock (complex services)

import { createMock } from '@golevelup/ts-jest';
import { ExecutionContext } from '@nestjs/common';

const mockExecutionContext = createMock<ExecutionContext>();

https://github.com/golevelup/nestjs/tree/master/packages/testing

@golevelup/ts-jest

What's next ?

Profile our tests to identify other recurrent tasks

Use the new sharding feature of Jest

Opening

Are we following the correct KPIs ?

Opening

Are we following the correct KPIs ?

450
tests

830
tests

Opening

Are we following the correct KPIs ?

A tool to mesure your performance

https://github.com/LeoAnesi/github-ci-analysis

Merci !

NestJs integration tests

By Léo Anesi

NestJs integration tests

  • 265