Pourquoi Tester NestJS avec Jest ?
Dans un environnement professionnel, il est essentiel d'avoir une couverture de tests robuste pour assurer la qualité et la fiabilité de nos applications. Les développeurs NestJS ont souvent besoin d'un outil puissant pour tester leurs applications backend. Jest est l'une des solutions les plus populaires pour ce genre de tâche. Il offre un ensemble complet de fonctionnalités pour tester du code simple aux systèmes complexes, tout en facilitant la création et le maintien de tests efficaces.
Un cas d'utilisation concret : imaginez que vous développez une API RESTful pour gérer les utilisateurs de votre application. Vous voudriez vous assurer que chaque endpoint fonctionne correctement, que toutes les validations des données sont effectuées, et que la gestion des erreurs est appropriée.
Prerequis
- Connaissance approfondie de NestJS
- Familiarité avec TypeScript
- Jest installé sur votre machine (version recommandée : 26.0.1)
- Node.js et npm installés (versions recommandées : Node.js v14.x ou v16.x, npm v7.x)
Concepts fondamentaux
1. Installation de Jest
Avant de commencer à tester votre projet NestJS avec Jest, vous devez l'installer. Ouvrez un terminal et exécutez la commande suivante :
npm install --save-dev jest @nestjs/testing ts-jest
Cela installe Jest, le module @nestjs/testing, et ts-jest pour permettre à Jest de travailler avec TypeScript.
2. Configuration de Jest
Créez un fichier jest.config.js à la racine de votre projet et ajoutez les configurations suivantes :
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
coverageDirectory: 'coverage',
};
3. Création d'un Test pour un Service
Supposons que vous ayez un service UserService dans votre projet NestJS. Voici comment vous pouvez créer un test pour ce service :
// src/user/user.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
private readonly users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' },
];
findAll(): any[] {
return this.users;
}
findOne(id: number): any {
return this.users.find(user => user.id === id);
}
}
Pour tester ce service, créez un fichier user.service.spec.ts :
// src/user/user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('findAll() should return an array of users', () => {
const result = service.findAll();
expect(result).toEqual([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' },
]);
});
it('findOne(1) should return the user with id 1', () => {
const result = service.findOne(1);
expect(result).toEqual({ id: 1, name: 'John Doe' });
});
});
4. Exécution des Tests
Pour exécuter vos tests, utilisez la commande suivante :
npx jest
Cela lancera les tests et vous affichera le résultat dans votre terminal.
Mise en pratique : projet fil rouge
Dans cette section, nous allons construire un mini-projet complet et réaliste en utilisant NestJS et Jest. Nous allons créer une API de blog simple qui permet d'ajouter des articles et de les récupérer.
Étape 1 : Création du Projet
Commencez par créer un nouveau projet NestJS :
nest new blog-api
cd blog-api
Étape 2 : Création du Controller
Créez un controller posts.controller.ts pour gérer les endpoints de l'API :
// src/posts/posts.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { PostsService } from './posts.service';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Post()
create(@Body() post: any) {
return this.postsService.create(post);
}
@Get()
findAll() {
return this.postsService.findAll();
}
}
Étape 3 : Création du Service
Créez un service posts.service.ts pour gérer la logique métier :
// src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class PostsService {
private readonly posts = [];
create(post: any) {
this.posts.push(post);
return post;
}
findAll(): any[] {
return this.posts;
}
}
Étape 4 : Création du Test pour le Controller
Créez un fichier posts.controller.spec.ts pour tester le controller :
// src/posts/posts.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
describe('PostsController', () => {
let controller: PostsController;
let service: PostsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PostsController],
providers: [
PostsService,
{
provide: 'POSTS_SERVICE',
useValue: {},
},
],
}).compile();
controller = module.get<PostsController>(PostsController);
service = module.get<PostsService>('POSTS_SERVICE');
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create()', () => {
it('should create a post', () => {
const post = { title: 'Test Post', content: 'This is a test post' };
jest.spyOn(service, 'create').mockImplementation(() => post);
expect(controller.create(post)).toBe(post);
});
});
describe('findAll()', () => {
it('should return an array of posts', () => {
const posts = [{ title: 'Post 1', content: 'Content 1' }, { title: 'Post 2', content: 'Content 2' }];
jest.spyOn(service, 'findAll').mockImplementation(() => posts);
expect(controller.findAll()).toBe(posts);
});
});
});
Étape 5 : Exécution des Tests
Exécutez les tests en utilisant la commande suivante :
npx jest
Erreurs frequentes et debugging
1. TypeError: Cannot read property 'mockImplementation' of undefined
Erreur liée à l'utilisation incorrecte du mock.
// ❌ Mauvais
jest.spyOn(service, 'create').mockImplementation(() => post);
// ✅ Correct
const spy = jest.spyOn(service, 'create');
spy.mockImplementation(() => post);
2. Error: Expected 1 to be undefined
Erreur liée à une assertion incorrecte.
// ❌ Mauvais
expect(controller.create(post)).toBeUndefined();
// ✅ Correct
expect(controller.create(post)).toEqual(post);
3. SyntaxError: Unexpected token import
Erreur liée au format de code TypeScript.
// ❌ Mauvais
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [],
providers: [],
})
export class AppModule {}
// ✅ Correct
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [],
providers: [],
})
export class AppModule {}
Pour aller plus loin
1. Utilisation de Spies pour les fonctions asynchrones
Vous pouvez utiliser des spies pour tester les fonctions asynchrones.
// src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class PostsService {
private readonly posts = [];
async create(post: any) {
this.posts.push(post);
return post;
}
async findAll(): Promise<any[]> {
return new Promise(resolve => resolve(this.posts));
}
}
typescript
// src/posts/posts.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
describe('PostsController', () => {
let controller: PostsController;
let service: PostsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PostsController],
providers: [
PostsService,
{
provide: 'POSTS_SERVICE',
useValue: {},
},
],
}).compile();
controller = module.get<PostsController>(PostsController);
service = module.get<PostsService>('POSTS_SERVICE');
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create()', () => {
it('should create a post', async () => {
const post = { title: 'Test Post', content: 'This is a test post' };
jest.spyOn(service, 'create').mockImplementation(() => Promise.resolve(post));
expect(await controller.create(post)).toEqual(post);
});
});
describe('findAll()', () => {
it('should return an array of posts', async () => {
const posts = [{ title: 'Post 1', content: 'Content 1' }, { title: 'Post 2', content: 'Content 2' }];
jest.spyOn(service, 'findAll').mockImplementation(() => Promise.resolve(posts));
expect(await controller.findAll()).toEqual(posts);
});
});
});
2. Utilisation de Mocks pour les dépendances externes
Vous pouvez utiliser des mocks pour tester les dépendances externes.
// src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class PostsService {
private readonly posts = [];
async create(post: any) {
this.posts.push(post);
return post;
}
async findAll(): Promise<any[]> {
return new Promise(resolve => resolve(this.posts));
}
}
typescript
// src/posts/posts.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
describe('PostsController', () => {
let controller: PostsController;
let service: PostsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PostsController],
providers: [
PostsService,
{
provide: 'POSTS_SERVICE',
useValue: {},
},
],
}).compile();
controller = module.get<PostsController>(PostsController);
service = module.get<PostsService>('POSTS_SERVICE');
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create()', () => {
it('should create a post', async () => {
const post = { title: 'Test Post', content: 'This is a test post' };
jest.spyOn(service, 'create').mockImplementation(() => Promise.resolve(post));
expect(await controller.create(post)).toEqual(post);
});
});
describe('findAll()', () => {
it('should return an array of posts', async () => {
const posts = [{ title: 'Post 1', content: 'Content 1' }, { title: 'Post 2', content: 'Content 2' }];
jest.spyOn(service, 'findAll').mockImplementation(() => Promise.resolve(posts));
expect(await controller.findAll()).toEqual(posts);
});
});
});
3. Utilisation de Mocks pour les middlewares
Vous pouvez utiliser des mocks pour tester les middlewares.
// src/posts/posts.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class PostsMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Posts Middleware');
next();
}
}
typescript
// src/posts/posts.controller.ts
import { Controller, Get, UseMiddleware } from '@nestjs/common';
import { PostsService } from './posts.service';
import { PostsMiddleware } from './posts.middleware';
@Controller('posts')
@UseMiddleware(PostsMiddleware)
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Get()
findAll() {
return this.postsService.findAll();
}
}
typescript
// src/posts/posts.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { PostsMiddleware } from './posts.middleware';
describe('PostsController', () => {
let controller: PostsController;
let service: PostsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PostsController],
providers: [
PostsService,
{
provide: 'POSTS_SERVICE',
useValue: {},
},
],
}).compile();
controller = module.get<PostsController>(PostsController);
service = module.get<PostsService>('POSTS_SERVICE');
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('findAll()', () => {
it('should return an array of posts', async () => {
const posts = [{ title: 'Post 1', content: 'Content 1' }, { title: 'Post 2', content: 'Content 2' }];
jest.spyOn(service, 'findAll').mockImplementation(() => Promise.resolve(posts));
expect(await controller.findAll()).toEqual(posts);
});
});
});
Défi pratique
Défi : Créer un test pour une fonction asynchrone dans un service
- Créez un service
users.service.tsavec une méthodefindByName(name: string)qui retourne un utilisateur en fonction de son nom. - Créez un test pour cette méthode dans
users.service.spec.ts. - Assurez-vous que le test vérifie correctement les différents cas (utilisateur trouvé, utilisateur non trouvé).
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' },
];
async findByName(name: string): Promise<any> {
return new Promise(resolve => resolve(this.users.find(user => user.name === name)));
}
}
typescript
// src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('findByName()', () => {
it('should return the user with name "John Doe"', async () => {
const user = await service.findByName('John Doe');
expect(user).toEqual({ id: 1, name: 'John Doe' });
});
it('should return undefined if no user is found', async () => {
const user = await service.findByName('Jane Smith');
expect(user).toBeUndefined();
});
});
});
En suivant ce défi, vous pouvez vous assurer que vous maîtrisez bien la façon de tester des fonctions asynchrones et des services dans NestJS.