Aller au contenu principal

Test d'un CRUD (contrôleur)

Créer le fichier de test

  1. Créer un fichier de test avec un nom significatif pour ce que vous testez, dans le dossier tests. Par exemple, tests/projets.e2e-spec.ts ici.
  2. Insérez la base du test pour obtenir la liste de projets
test/projets.e2e-spec.ts
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../src/app.module';

describe('ProjetsController (e2e)', () => {
let app: INestApplication;

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

it('devrait retourner la liste des projets', async () => {

});

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

Créer des fixtures

En mode test, les tests sont responsables d'insérer dans la base de données les données nécessaires aux tests. Pour cela, on créera des fixtures qui sont des données statiques qu'on peut utiliser pour les insertions de données de test.

  1. Créer un dossier fixtures sous le dossier test
  2. Créer un fichier projets.fixture.ts
test/fixtures/projets.fixture.ts
export const projetsFixture = [
{
nom: "Projet de test",
description: "Projet pour les tests",
image_url: null,
},
{
nom: "Autre projet de test",
description: "Projet pour les tests",
image_url: "https://i.i.imgur.com/Y5nZ4Qe.jpg",
},
];
info

Les projets ne contiennent pas de id, ni de date de création/modification puisque le tout sera sauvegardé dans la BD et ces informations nous proviendront de l'insertion en BD.

Insérer les données dépendantes pour le test

Afin de récupérer une liste de projets, il est nécessaire d'avoir des projets! On peut ainsi utiliser la fonction save d'une entité pour sauvegarder des données test dans la BD.

describe('ProjetsController (e2e)', () => {
let app: INestApplication;

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

it('devrait retourner la liste des projets', async () => {
await Projet.save(projetsFixture as DeepPartial<Projet>[]);
});

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

Première assertion (statut 200)

On peut essayer de vérifier qu'en effectuant une requête GET vers /projets, qu'on obtient bien une réponse avec un statut 200.

it('devrait retourner la liste des projets', async () => {
await Projet.save(projetsFixture as DeepPartial<Projet>[]);

const reponse = await request(app.getHttpServer()).get('/projets')
expect(reponse.statusCode).toBe(200);
});

Cependant, en exécutant les tests via npm run test:e2e, vous obtiendrez une erreur du genre:

ProjetsController (e2e) › devrait retourner la liste des projets

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 401

401 signifie une erreur d'authentification. En effet, l'action permettant d'obtenir la liste des serveurs est protégée par un guard et on doit être identifié.

Créer un jeton d'authentification pour les tests

Il nous faut un jeton pour effectuer les requêtes qui nécessitent d'être authentifiées. Nous allons créer une petite fonction utilitaire permettant de signer un jeton. Nous pourrons utiliser cette fonction lors des tests.

  1. Créer un fichier de fixture pour les utilisateurs (utilisateurs.fixture)
    test/fixtures/utilisateurs.fixture.ts
    import * as bcrypt from 'bcrypt';

    export const utilisateurFixture = {
    prenom: 'Uti',
    nom: 'Lisateur',
    courriel: 'u@ser.com',
    nomUtilisateur: 'user',
    role: Role.Admin,
    password: bcrypt.hashSync('p@ssword', 12),
    };
    info

    J'ai mis ici l'utilisateur comme étant Admin, mais vous pourriez avoir des tests très pertinents avec un utilisateur 'utilisateur' qui vérifie que certaines actions ne sont pas disponibles, par exemple.

  2. Lors de chaque test, créer un utilisateur (n'oubliez pas que la bd est réinitialisée avant chaque test pour assurer un environnement contrôlé!)
    describe('ProjetsController (e2e)', () => {
    let app: INestApplication;
    let utilisateur: Utilisateur;

    beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
    utilisateur = await Utilisateur.save(utilisateurFixture as DeepPartial<Utilisateur>);
    });
  3. Créer un dossier helpers sous le dossier tests
  4. Ajouter un nouveau fichier auth.helper.ts
  5. Ajoutez la fonction qui vous permettra de signer un jeton
    test/helpers/auth.helpter.ts
    import { JwtService } from '@nestjs/jwt';
    import { Utilisateur } from '../../src/utilisateurs/entities/utilisateur.entity';

    export function genererJetonJwt(jwtService: JwtService, utilisateur: Partial<Utilisateur>): string {
    return jwtService.sign({
    sub: utilisateur.id,
    courriel: utilisateur.courriel,
    nomUtilisateur: utilisateur.nomUtilisateur,
    role: utilisateur.role,
    });
    }
  6. Signez un jeton pour l'utilisateur avant chaque test
    describe('ProjetsController (e2e)', () => {
    let app: INestApplication;
    let utilisateur: Utilisateur;
    let accessToken: string;

    beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
    utilisateur = await Utilisateur.save(utilisateurFixture as DeepPartial<Utilisateur>);

    const jwtService = moduleFixture.get<JwtService>(JwtService);
    accessToken = genererJetonJwt(jwtService, utilisateur);
    });
  7. Lors du test, ajoutez le jeton à la requête
    test/projets.e2e-spec.ts
    it('devrait retourner la liste des projets', async () => {
    await Projet.save(projetsFixture as DeepPartial<Projet>);

    const reponse = await request(app.getHttpServer())
    .get('/projets')
    .set('Authorization', `Bearer ${accessToken}`);
    expect(reponse.statusCode).toBe(200);
    });

Assertion de taille d'un tableau (body)

En plus de vérifier le statut, on peut aussi vérifier le contenu de la réponse.

it('devrait retourner la liste des projets', async () => {
await Projet.save(projetsFixture as DeepPartial<Projet>);

const reponse = await request(app.getHttpServer())
.get('/projets')
.set('Authorization', `Bearer ${accessToken}`);

expect(reponse.statusCode).toBe(200);
expect(reponse.body).toHaveLength(projetsFixture.length);
});

Vérifier les règles d'autorisation

it('devrait être impossible d\'obtenir la liste sans être connecté', async () => {
const reponse = await request(app.getHttpServer())
.get('/projets');

expect(reponse.statusCode).toBe(401);
});

Test pour récupérer un élément et tester le retour

  1. Ajouter une nouvelle fixture pour un seul élément dans le fichier de fixtures des projets
test/fixtures/projets.fixture.ts
export const projetFixture =   {
nom: "Projet de test pour sauvegarde",
description: "Projet pour les tests",
image_url: "https://i.i.imgur.com/Y5nZ4Qe.jpg",
}
  1. Créer un nouveau test et faire la requête authentifiée
test/projets.e2e-spec.ts
it('devrait retourner un projet', async () => {
const projet = await Projet.save(projetFixture as DeepPartial<Projet>);

const reponse = await request(app.getHttpServer())
.get(`/projets/${projet.id}`)
.set('Authorization', `Bearer ${token}`);
});
  1. Vérifier la réponse
test/projets.e2e-spec.ts
it('devrait retourner un projet', async () => {
const projet = await Projet.save(projetFixture as DeepPartial<Projet>);

const reponse = await request(app.getHttpServer())
.get(`/projets/${projet.id}`)
.set('Authorization', `Bearer ${token}`);

expect(reponse.statusCode).toBe(200);
expect(reponse.body).toHaveProperty('nom');
expect(reponse.body.nom).toBe(projet.nom);
})

Test de création (POST)

Pour vérifier qu'un POST fonctionne, et donc qu'un nouvel item a été créé, on peut vérifier que le nombre d'éléments a changé dans la base de données après l'opération.

test/projets.e2e-spec.ts
it('devrait créer un projet', async () => {
const projetsCountInitial = await Projet.count();

const reponse = await request(app.getHttpServer())
.post('/projets')
.set('Authorization', `Bearer ${accessToken}`)
.send(projetFixture);

expect(reponse.statusCode).toBe(201); // Vérification du statut (Created)
expect(reponse.body).toHaveProperty('nom'); // Vérification présence propriété
expect(reponse.body.nom).toBe(projetFixture.nom); // Vérification contenu objet
expect(await Projet.count()).toBe(projetsCountInitial+1); // Vérification qu'une entrée de plus est dans la BD
});

Test de mise à jour (PATCH)

test/projets.e2e-spec.ts
it('devrait modifier un projet', async () => {
const projetInitial = await Projet.save(projetFixture as DeepPartial<Projet>);

const nouveauNom = 'Le projet a été modifié';
const reponse = await request(app.getHttpServer())
.patch(`/projets/${projetInitial.id}`)
.set('Authorization', `Bearer ${accessToken}`)
.send({
nom: nouveauNom
});

expect(reponse.statusCode).toBe(200);
expect(reponse.body).toHaveProperty('nom');
expect(reponse.body.nom).toBe(nouveauNom);
});

Test de suppression (DELETE)

À l'image du test de création, pour ce qui est de la suppression, on peut vérifier que le nombre d'éléments en BD a été réduit de 1.

test/projets.e2e-spec.ts
it('devrait supprimer un projet', async () => {
const projet = await Projet.save(projetFixture);
const projetsCountInitial = await Projet.count();

const reponse = await request(app.getHttpServer())
.delete(`/projets/${projet.id}`)
.set('Authorization', `Bearer ${accessToken}`)

expect(reponse.statusCode).toBe(200);
expect(await Projet.count()).toBe(projetsCountInitial-1);
});