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
- 246