logo
  • icon
    • Accueilicon
    • Projetsicon
    • Articlesicon
    • Me contactericon
    • A proposicon
Logo mobile
logo

La confiance en soi s'acquiert par l'expérience et le travail acharné

Tiavina Michael RALAINIRINA linkedinTiavina Michael RALAINIRINA linkedin
Tiavina Michael RALAINIRINA whatsappTiavina Michael RALAINIRINA whatsapp
Tiavina Michael RALAINIRINA instagramTiavina Michael RALAINIRINA instagram
Tiavina Michael RALAINIRINA emailTiavina Michael RALAINIRINA email
Tiavina Michael RALAINIRINA skypeTiavina Michael RALAINIRINA skype
Tiavina Michael RALAINIRINA youtubeTiavina Michael RALAINIRINA youtube

Plus d'information

Contactez-moiMes projetsPlan du siteFAQPolitique de confidentialité

Contact

VN 50B, Ambohitsoa

Antananarivo, Madagascar

tiavinamika@gmail.com

Mika © 2025. Tous droits réservés.

Technos

Test d'isolement complet avec Jest, MongoDB et Express

Jest simplifient la création et la gestion des tests en fournissant des fonctionnalités comme l’exécution isolée pour chaque fichier. Cependant, l’intégration de MongoDB via une base de données en mémoire (par exemple, @shelf/jest-mongodb ou mongodb-memory-server) peut poser des problèmes d’isolation des tests.

Test d'isolement complet avec Jest, MongoDB et Express

RT

Tiavina Michael Ralainirina

29 Nov 2024

Les tests sont une étape cruciale du développement logiciel moderne, garantissant que le code se comporte comme prévu dans diverses situations. Des outils comme Jest simplifient la création et la gestion des tests en fournissant des fonctionnalités comme l’exécution isolée pour chaque fichier. Cependant, l’intégration de MongoDB via une base de données en mémoire (par exemple, @shelf/jest-mongodb ou mongodb-memory-server) peut poser des problèmes d’isolation des tests.

Les défis de l'isolation des tests avec MongoDB et Jest

Les tests unitaires sont essentiels pour garantir la qualité et la fiabilité d'une application. Des outils comme Jest simplifient grandement ce processus en offrant des fonctionnalités comme l'exécution isolée de chaque test. Cependant, lorsqu'on intègre une base de données comme MongoDB dans nos tests, des challenges spécifiques émergent, notamment en ce qui concerne l'isolation des tests.

Le problème de l'état partagé

Par défaut, les bibliothèques comme @shelf/jest-mongodb et mongodb-memory-server initialisent une seule instance de MongoDB en mémoire pour tous les tests. Bien que cela puisse accélérer les exécutions, cela entraîne un partage de l'état de la base de données entre les différents tests.

Pourquoi ?

  • Base de données unique: Tous les tests utilisent la même base de données, ce qui signifie que les modifications effectuées par un test peuvent affecter les résultats des tests suivants.

  • Limites de Jest: Bien que Jest exécute chaque fichier de test dans un processus séparé, la base de données MongoDB reste un point d'ancrage commun à tous ces processus.

Conséquences

  • Résultats non reproductibles: L'ordre d'exécution des tests peut influencer les résultats, rendant les tests moins fiables.

  • Difficultés de débogage: Il devient plus compliqué d'isoler les causes d'échecs, car les problèmes peuvent être liés à des interactions inattendues entre les tests.

  • Violation des principes fondamentaux des tests unitaires: L'isolation des tests est un principe clé qui est compromis lorsque les tests partagent un état.

Pourquoi MongoDB est différent des bases de données SQL traditionnelles

Les bases de données SQL offrent souvent des mécanismes de transaction qui permettent d'annuler les modifications apportées à la base de données à la fin d'un test. MongoDB, en revanche, ne supporte pas nativement les transactions dans toutes les configurations, ce qui rend plus difficile de garantir l'isolation des tests.

Installation des packages nécessaires

yarn add --dev @shelf/jest-mongodb jest ts-jest typescript


Configurer jest

// jest.config.js

module.exports = {
  preset: '@shelf/jest-mongodb',
  testEnvironment: 'node',
  testMatch: ['**/__tests__/?(*.)+(spec|test).ts'],
  moduleFileExtensions: ['ts'],
  watchPathIgnorePatterns: ['globalConfig'],
  testPathIgnorePatterns: ['/node_modules/'],
  transform: {
    '^.+\\.(ts)$': 'ts-jest',
  }
};

Pour exécuter lest tests, il faut ajouter un script test dans package.json.

  "scripts": {
    "test": "jest",
  },

Automatiser avec un fichier de configuration global

Plutôt que de configurer manuellement les serveurs et bases de données dans chaque fichier de test, on peut automatiser ce processus avec un utilitaire centralisé et une configuration globale dans Jest.

Configuration dans Jest

Jest permet de définir des hooks globaux via des fichiers de configuration, comme beforeAll et afterAll, qui seront automatiquement exécutés pour chaque fichier de test.

  1. Avant chaque fichier de test (beforeAll)

    • Initialiser une nouvelle instance de serveur Express.

    • Créer une base de données MongoDB unique pour le fichier.

    • Attacher cette base de données au serveur pour que chaque test dans le fichier y accède facilement.

  2. Après chaque fichier de test (afterAll)

    • Supprimer la base de données créée pour le fichier.

    • Arrêter le serveur Express.

  3. Libérer les ressources associées (fermeture des connexions MongoDB, arrêt des instances en mémoire).

    Créer un fichier jest.setup.ts dans le dossier __tests__/

//  __tests__/jest.setup.ts
import { MongoClient } from 'mongodb';
import crypto from 'crypto';
import { deleteTestDatabases, generateRandomPort } from './utils';

let connection: MongoClient;

beforeAll(() => {
  // appelé avant chaque fichier de test
});

afterAll(() => {
    // appelé après chaque fichier de test
});

Puis ajouter cette ligne dans jest.config.js

setupFilesAfterEnv: ['./__tests__/jest.setup.ts']

Solutions

  1. Une base de données unique pour chaque test

    Pour résoudre ce problème, il est essentiel que chaque fichier de test utilise sa propre base de données. Cette solution peut être mise en œuvre en :

    Pour assurer l'intégrité des tests, on réinitialise la base de données à un état connu avant chaque test. Cela garantit que chaque cas de test s'exécute dans un environnement propre, sans être influencé par les tests précédents.

    Pour cela il est essentiel que chaque fichier de test utilise sa propre base de données


Créer et nettoyer la base de données

// __tests__/jest.setup.ts
import { MongoClient } from 'mongodb';
import crypto from 'crypto';
import { deleteTestDatabases, generateRandomPort } from './utils';

let connection: MongoClient;

beforeAll(async () => {
  const port = generateRandomPort();
  const randomText = crypto.randomUUID();

  const mongoUrl = process.env.MONGO_URL + 'test_' + randomText;
  
  connection = await MongoClient.connect(mongoUrl, {});
});

afterAll(async () => {
    await deleteTestDatabases(connection);
});

Explication

  • beforeAll : Avant chaque série de tests, un nom de base de données unique est généré.

  • afterAll : À la fin de chaque série de tests, la base de données créée est supprimée.

  • Fonction de nettoyage : Une fonction est mise en place pour nettoyer l'environnement en supprimant toutes les bases de données créées pour les tests.

// __tests__/utils.ts
import { ListDatabasesResult, MongoClient } from 'mongodb';

export const deleteTestDatabases = async (connection: MongoClient) => {
  try {
    const databaseNames = await connection.db().admin().listDatabases();
    const testDatabaseNames = databaseNames.databases
      .filter((db: ListDatabasesResult['databases'][0]) => db.name.startsWith('test_'));

    for (const database of testDatabaseNames) {
      await connection.db(database.name).dropDatabase();
      console.log(`Deleted database: ${database.name}`);
    }
  }
  catch (error) {
    console.error('Error deleting test databases:', error);
  }
  finally {
    await connection.close();
  }
};

Ici on supprime les base de données dont les noms commencent par test_

2. Un serveur distinct par fichier de test

Chaque fichier de test doit initialiser son propre serveur et sa propre base de données

Installer Express et Parse Server

yarn add express parse-server

Modifier jest.setup.js

// __tests__/jest.setup.ts
import http from 'http';
import { MongoClient } from 'mongodb';
import { ParseServer } from 'parse-server';
import express from 'express';
import crypto from 'crypto';
import { deleteTestDatabases, generateRandomPort } from './utils';

let parseServer;
let server: http.Server;
let connection: MongoClient;

beforeAll(async () => {
  const port = generateRandomPort();
  const randomText = crypto.randomUUID();

  const appId = 'testAppId' + randomText;
  const serverURL = `http://localhost:${port}/parse`;

  const mongoUrl = process.env.MONGO_URL + 'test_' + randomText;
  
  // Connect to MongoDB
  connection = await MongoClient.connect(mongoUrl, {});

  // Configure Parse Server with MongoDB URI
  parseServer = new ParseServer({
    databaseURI: mongoUrl,
    appId,
    serverURL,
  });

  // Start the Parse Server
  await parseServer.start();
  const app = express();

  // Mount the Parse Server on the Express app
  app.use('/parse', parseServer.app);
  // Start the Express server
  server = app.listen(port, undefined);

});

afterAll(async () => {
  await parseServer.handleShutdown();
  server.close();
  await deleteTestDatabases(connection);
});


Créer les fichiers de tests

Créer 2 cas de tests indépendants.

// __tests__/article.spec.ts
import { createArticleByAuthor, createUser, getUser } from "./mock";

describe('Article test', () => {
  let Article: Parse.ObjectConstructor;

  let author: Parse.User;

  beforeAll(async () => {
    Article = Parse.Object.extend('Article');
    author = await createUser()
  });

  it('should user in author test case not defined', async () => {
    const user = await getUser('user');
    expect(user).toBeUndefined();
  });

  it('should create article', async () => {
    const article = await createArticleByAuthor(author);
    expect(article).toBeDefined();
  });

  it('should author not defined', async () => {
    try {
      await createArticleByAuthor(undefined);
    } catch (error) {
      expect((error as Error).message).toBe('Author is required');
    }
  });
});

// __tests__/author.spec.ts
import { createUser, deleteUser, getUser } from "./mock";

describe('Author test', () => {
  it('should user in article test case not defined', async () => {
    const user = await getUser('john');
    expect(user).toBeUndefined();
  });

  it('should create user', async () => {
    const user = await createUser();
    expect(user).toBeDefined();
  });

  it('should get user', async () => {
    const user = await getUser('john');
    expect(user).toBeDefined();
  });

  it('should get user not exist', async () => {
    const user = await getUser('john2');
    expect(user).toBeUndefined();
  });

  it('should delete author', async () => {
    await deleteUser('john');
    const user = await getUser('john');
    expect(user).toBeUndefined();
  });
});

La partie la plus importante de ces deux fichiers est :

  it('should user in author test case not defined', async () => {
    const user = await getUser('user');
    expect(user).toBeUndefined();
  });
  it('should user in author test case not defined', async () => {
    const user = await getUser('user');
    expect(user).toBeUndefined();
  })

Défis

  • Coût en termes de performance: Les réinitialisations fréquentes de la base de données peuvent engendrer un ralentissement significatif des tests, en particulier pour les bases de données volumineuses ou complexes.

  • Intégrité des données: Maintenir l'intégrité et la cohérence des données après chaque réinitialisation est un défi majeur, car toute anomalie peut compromettre la fiabilité des résultats.

  • Consommation de ressources: Cette stratégie peut être gourmande en ressources, notamment en termes de calcul, ce qui limite son utilisation dans certains environnements.

  • Complexité de mise en œuvre: La configuration et la maintenance d'un système de réinitialisation peuvent être complexes, surtout pour des applications avec des interdépendances importantes.

Avantages

  • Environnement de test propre: La réinitialisation permet de commencer chaque test dans un environnement vierge, sans aucune donnée résiduelle des tests précédents.

  • Isolation des tests: Cette méthode est particulièrement adaptée aux scénarios de test complexes où les interactions entre les données peuvent être difficiles à isoler.

  • Flexibilité: La réinitialisation de la base de données offre une grande flexibilité et s'adapte à une variété de scénarios de test.

  • Fiabilité des résultats: En garantissant un point de départ identique pour chaque test, cette stratégie améliore considérablement la fiabilité et la reproductibilité des résultats.

Conclusion

Mettre en place des tests unitaires rigoureux pour votre application Node.js Express qui utilise MongoDB peut sembler complexe. Pourtant, en suivant les étapes détaillées de ce guide, vous pourrez rapidement créer des tests isolés et efficaces. Pour une démonstration concrète, n'hésitez pas à consulter le code source complet disponible sur mon dépôt GitHub:
https://github.com/tiavina-mika/blog/tree/main/jest-mongodb-express-parse-server

iconiconiconiconiconiconicon

Tags:

Nodejs

,

Test

,

Express

,

Parse Server

,

TypeScript

,

Test d'intégration

,

Jest

Articles similaires

S'abonner à ma newsletter

Abonnez-vous à ma newsletter pour pouvoir suivre et récevoir des offres spéciales et les articles / tutos que je publie occasionnellement sur mon blog

* Vous pouvez se désabonner à tout moment en cliquant sur le lien de désabonnement contenu dans chacun de nos mails.

Recherche intelligente avec FlexSearch et Material-UI dans React
Recherche intelligente avec FlexSearch et Material-UI dans React

Implémentation d'une autocomplétion de documents en utilisant FlexSearch et Material-UI dans un projet React. FlexSearch gérera l'indexation rapide et la recherche, tandis que Material-UI fournira une interface épurée pour le champ de recherche et le menu déroulant des suggestions.