Partager via


Guide pratique pour tester l’intégration du Kit de développement logiciel (SDK) Azure dans des applications JavaScript

Tester votre code d’intégration pour le SDK Azure pour JavaScript est essentiel pour garantir que vos applications interagissent correctement avec les services Azure. Ce guide vous montre comment tester efficacement l’intégration du Kit de développement logiciel (SDK) Azure dans vos applications JavaScript une infrastructure de test.

Lorsqu’il s’agit de décider s’il faut simuler les appels du SDK des services cloud ou utiliser un service réel à des fins de test, il est important de considérer les compromis entre rapidité, fiabilité et coût. Cet article montre comment utiliser une infrastructure de test pour tester l’intégration du Kit de développement logiciel (SDK). Le code de l’application insère un document dans Cosmos DB. Le code de test simulant cette utilisation des ressources afin que la ressource cloud ne soit pas utilisée.

Les frameworks utilisés sont les suivants :

  • Jest avec CommonJs
  • Vitest avec ESM
  • Node.js Test Runner avec ESM

Prérequis

Node.js LTS. Le statut de version LTS est « support à long terme », ce qui garantit généralement que les bogues critiques seront corrigés pendant un total de 30 mois.

L’exécuteur de testNode.js fait partie de l’installation Node.js.

Avertissement

L’exemple fourni pour l’exécuteur de test Node.js utilise le module expérimental node :test avec mock.fn(). N'oubliez pas que l'environnement de test intégré de Node.js n'offre pas encore d'API de simulation entièrement prise en charge. Assurez-vous que votre version de nœud cible prend en charge les API expérimentales ou envisagez d’utiliser une bibliothèque de simulation tierce (ou des fonctions stub) à la place.

Simuler les services cloud

Avantages :

  • Accélère la suite de tests en éliminant la latence du réseau.
  • Fournit des environnements de test prévisibles et contrôlés.
  • Facilite la simulation de divers scénarios et cas limites.
  • Réduit les coûts associés à l’utilisation de services cloud réels, en particulier dans les pipelines d’intégration continue.

Inconvénients :

  • Les simulations peuvent dériver du SDK réel, ce qui entraîne des différences.
  • Peut ignorer certaines fonctionnalités ou comportements du service réel.
  • Environnement moins réaliste par rapport à la production.

Utilisation d’un service réel

Avantages :

  • Un environnement réaliste qui reflète étroitement la production ?
  • Est-il utile pour les tests d’intégration de garantir que différentes parties du système fonctionnent ensemble ?
  • Est-il utile d’identifier les problèmes liés à la fiabilité du réseau, à la disponibilité du service et à la gestion réelle des données ?

Inconvénients :

  • Est plus lent en raison des appels réseau.
  • Est plus coûteux en raison des coûts potentiels d’utilisation du service.
  • Est complexe et fastidieux pour configurer et gérer un environnement de service en direct qui correspond à la production.

Le choix entre la simulation et l’utilisation de services réels dépend de votre stratégie de test. Pour les tests unitaires où la rapidité et le contrôle sont primordiaux, la simulation est souvent le meilleur choix. Pour les tests d’intégration où le réalisme est crucial, l’utilisation d’un service réel peut fournir des résultats plus précis. Équilibrer ces approches aide à obtenir une couverture de test complète tout en gérant les coûts et en maintenant l’efficacité des tests.

Doubles de test : Mocks, stubs et fakes

Un double de test est tout type de substitut utilisé à la place de quelque chose de réel à des fins de test. Le type de double que vous choisissez dépend de ce que vous voulez remplacer. Le terme mock est souvent utilisé pour désigner tout double lorsqu’il est utilisé de manière informelle. Dans cet article, le terme est utilisé spécifiquement et illustré spécifiquement dans le framework de test Jest.

Simulations

Mocks (également appelés spies (espions)) : remplacez une fonction et soyez capable de contrôler et d’espionner le comportement de cette fonction lorsqu’elle est appelée indirectement par un autre code.

Dans les exemples suivants, vous avez 2 fonctions :

  • someTestFunction: fonction que vous devez tester. Elle appelle une dépendance, dependencyFunction, que vous n’avez pas écrite et que vous n’avez pas besoin de tester.
  • dependencyFunctionMock : mock de la dépendance.
import { mock } from 'node:test';
import assert from 'node:assert';

// ARRANGE
const dependencyFunctionMock = mock.fn();

// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// ASSERT
assert.strictEqual(dependencyFunctionMock.mock.callCount(), 1);

Le but du test est de s’assurer que someTestFunction se comporte correctement sans réellement invoquer le code de la dépendance. Le test valide que le mock de la dépendance a été appelé.

Simuler des dépendances grandes ou petites

Lorsque vous décidez de simuler une dépendance, vous pouvez choisir de simuler uniquement ce dont vous avez besoin, comme :

  • Une ou deux fonctions d’une grande dépendance. Jest propose des mocks partiels à cet effet.
  • Toutes les fonctions d’une petite dépendance, comme illustré dans l’exemple de cet article.

Reçus

L’objectif d’un stub est de remplacer les données de retour d’une fonction pour simuler différents scénarios. Vous utilisez un stub pour permettre à votre code d’appeler la fonction et de recevoir différents états, notamment les résultats réussis, les échecs, les exceptions et les cas de périphérie. La vérification de l’état garantit que votre code gère correctement ces scénarios.

import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';

// ARRANGE
const fakeDatabaseData = {first: 'John', last: 'Jones'};

const dependencyFunctionMock = mock.fn();
dependencyFunctionMock.mock.mockImplementation((arg) => {
    return fakeDatabaseData;
});

// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// ASSERT
assert.strictEqual(name, `${fakeDatabaseData.first} ${fakeDatabaseData.last}`);

L’objectif du test précédent est de s’assurer que le travail effectué par someTestFunction atteint le résultat attendu. Dans cet exemple simple, la tâche de la fonction est de concaténer les prénoms et les noms de famille. En utilisant des données factices, vous connaissez le résultat attendu et pouvez valider que la fonction effectue correctement le travail.

Faux

Les fakes remplacent une fonctionnalité que vous n’utiliseriez normalement pas en production, comme l’utilisation d’une base de données en mémoire au lieu d’une base de données cloud.

// fake-in-mem-db.spec.ts
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';

class FakeDatabase {
  private data: Record<string, any>;

  constructor() {
    this.data = {};
  }

  save(key: string, value: any): void {
    this.data[key] = value;
  }

  get(key: string): any {
    return this.data[key];
  }
}

// Function to test
function someTestFunction(db: FakeDatabase, key: string, value: any): any {
  db.save(key, value);
  return db.get(key);
}

describe('In-Mem DB', () => {
  let fakeDb: FakeDatabase;
  let testKey: string;
  let testValue: any;

  beforeEach(() => {
    fakeDb = new FakeDatabase();
    testKey = 'testKey';
    testValue = {
      first: 'John',
      last: 'Jones',
      lastUpdated: new Date().toISOString(),
    };
  });

  afterEach(() => {
    // Restore all mocks created by node:test’s mock helper.
    mock.restoreAll();
  });

  it('should save and return the correct value', () => {
    // Create a spy on the save method using node:test's mock helper.
    const saveSpy = mock.method(fakeDb, 'save').mock;

    // Call the function under test.
    const result = someTestFunction(fakeDb, testKey, testValue);

    // Verify state.
    assert.deepStrictEqual(result, testValue);
    assert.strictEqual(result.first, 'John');
    assert.strictEqual(result.last, 'Jones');
    assert.strictEqual(result.lastUpdated, testValue.lastUpdated);

    // Verify behavior
    assert.strictEqual(saveSpy.callCount(), 1);
    const calls = saveSpy.calls;
    assert.deepStrictEqual(calls[0].arguments, [testKey, testValue]);
  });
});

L’objectif du test précédent est de s’assurer que someTestFunction interagit correctement avec la base de données. En utilisant une fausse base de données en mémoire, vous pouvez tester la logique de la fonction sans dépendre d’une base de données réelle, rendant les tests plus rapides et plus fiables.

Scénario : Insérer un document dans Cosmos DB à l’aide du Kit de développement logiciel (SDK) Azure

Imaginez que vous avez une application qui doit écrire un nouveau document dans Cosmos DB si toutes les informations sont soumises et vérifiées. Si un formulaire vide est soumis ou si les informations ne correspondent pas au format attendu, l’application ne doit pas entrer les données.

Cosmos DB est utilisé à titre d’exemple, mais les concepts s’appliquent à la plupart des SDK Azure pour JavaScript. La fonction suivante capture cette fonctionnalité :

// insertDocument.ts
import { Container } from '../data/connect-to-cosmos.js';
import type {
  DbDocument,
  DbError,
  RawInput,
  VerificationErrors,
} from '../data/model.js';
import Verify from '../data/verify.js';

export async function insertDocument(
  container: Container,
  doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
  const isVerified: boolean = Verify.inputVerified(doc);

  if (!isVerified) {
    return { message: 'Verification failed' } as VerificationErrors;
  }

  try {
    const { resource } = await container.items.create({
      id: doc.id,
      name: `${doc.first} ${doc.last}`,
    });

    return resource as DbDocument;
  } catch (error: any) {
    if (error instanceof Error) {
      if ((error as any).code === 409) {
        return {
          message: 'Insertion failed: Duplicate entry',
          code: 409,
        } as DbError;
      }
      return { message: error.message, code: (error as any).code } as DbError;
    } else {
      return { message: 'An unknown error occurred', code: 500 } as DbError;
    }
  }
}

Remarque

Les types TypeScript aident à définir les types de données utilisés par une fonction. Bien que vous n’ayez pas besoin de TypeScript pour utiliser Jest ou d’autres frameworks de test JavaScript, il est essentiel d’écrire un code JavaScript de type sécurisé.

Les fonctions de cette application sont les suivantes :

Fonction Descriptif
insertDocument Insère un document dans la base de données. C’est ce que nous voulons tester.
inputVerified Vérifie les données d’entrée par rapport à un schéma. Assure que les données sont dans le bon format (par exemple, adresses e-mail valides, URL correctement formatées).
cosmos.items.create Fonction SDK pour Azure Cosmos DB utilisant le @azure/cosmos. C’est ce que nous voulons simuler. Elle a déjà ses propres tests maintenus par les propriétaires du package. Nous devons vérifier que l’appel de la fonction Cosmos DB a été effectué et a retourné des données si les données entrantes ont réussi la vérification.

Installer la dépendance du framework de test

Ce framework est fourni dans le cadre de Node.js LTS.

Configurer le package pour exécuter le test

Mettez à jour le package.json pour l’application avec un nouveau script pour tester nos fichiers de code source. Les fichiers de code source sont définis en fonction de la correspondance sur le nom partiel du fichier et de l’extension. Test Runner recherche des fichiers suivant la convention d’affectation de noms commune pour les fichiers de test : <file-name>.spec.[jt]s. Ce modèle signifie que les fichiers nommés comme les exemples suivants sont interprétés comme des fichiers de test et exécutés par Test Runner :

  • * .test.js : par exemple, math.test.js
  • * .spec.js : par exemple, math.spec.js
  • Fichiers situés dans un répertoire tests, comme tests/math.js

Ajoutez un script au package.json pour prendre en charge ce modèle de fichier de test avec Test Runner :

"scripts": {
    "test": "node --test --experimental-test-coverage --experimental-test-module-mocks --trace-exit"
}

Configurer le test unitaire pour le SDK Azure

Comment pouvons-nous utiliser des mocks, stubs et fakes pour tester la fonction insertDocument ?

  • Mocks : nous avons besoin d’un mock pour s’assurer que le comportement de la fonction est testé, par exemple :
    • Si les données passent la vérification, l’appel à la fonction Cosmos DB a eu lieu une seule fois
    • Si les données ne passent pas la vérification, l’appel à la fonction Cosmos DB n’a pas eu lieu
  • Stubs :
    • Les données passées correspondent au nouveau document retourné par la fonction.

Lors des tests, pensez en termes de configuration du test, du test lui-même et de la vérification. En termes de test vernaculaire, cette fonctionnalité utilise les termes suivants :

  • Préparer : configurez vos conditions de test
  • Agir : appelez votre fonction à tester, également connue sous le nom de système sous test ou SUT
  • Vérifier : validez les résultats. Les résultats peuvent être un comportement ou un état.
    • Le comportement indique la fonctionnalité dans votre fonction de test, qui peut être vérifiée. Un exemple est qu’une dépendance a été appelée.
    • L’état indique les données renvoyées par la fonction.
import { describe, it, afterEach, beforeEach, mock } from 'node:test';
import assert from 'node:assert';

describe('boilerplate', () => {
  beforeEach(() => {
    // Setup required before each test
  });
  afterEach(() => {
    // Cleanup required after each test
  });

  it('should <do something> if <situation is present>', async () => {
    // Arrange
    // - set up the test data and the expected result
    // Act
    // - call the function to test
    // Assert
    // - check the state: result returned from function
    // - check the behavior: dependency function calls
  });
});

Lorsque vous utilisez des simulations dans vos tests, ce code de modèle doit utiliser la simulation pour tester la fonction sans appeler la dépendance sous-jacente utilisée dans la fonction, comme les bibliothèques clientes Azure.

Créer le fichier de test

Le fichier de test avec des simulations, pour simuler un appel à une dépendance, a une configuration supplémentaire.

Il existe plusieurs parties dans le fichier de test :

  • import : les instructions d’importation vous permettent d’utiliser ou de simuler n’importe lequel de vos tests.
  • mock : créez le comportement de simulation par défaut que vous souhaitez. Chaque test peut être modifié au besoin.
  • describe : groupe de tests pour le fichier insert.ts.
  • it: chaque test pour le fichier insert.ts.

Le fichier de test couvre trois tests pour le fichier insert.ts, qui peuvent être divisés en deux types de validation :

Type de validation Essai
Chemin idéal : should insert document successfully La méthode de base de données simulée a été appelée et a renvoyé les données modifiées.
Chemin d’erreur : should return verification error if input is not verified Les données n’ont pas réussi la validation et ont renvoyé une erreur.
Chemin d’erreur : should return error if db insert fails La méthode de base de données simulée a été appelée et a renvoyé une erreur.

Le fichier de test suivant montre comment tester la fonction insertDocument .

// insertDocument.test.ts
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';

import { Container } from '../src/data/connect-to-cosmos.js';
import { createTestInputAndResult } from '../src/data/fake-data.js';
import type { DbDocument, DbError, RawInput } from '../src/data/model.js';
import { isDbError, isVerificationErrors } from '../src/data/model.js';

import Verify from '../src/data/verify.js';
import CosmosConnector from '../src/data/connect-to-cosmos.js';
import { insertDocument } from '../src/lib/insert.js';

describe('SDK', () => {
  beforeEach(() => {
    // Clear all mocks before each test
    mock.restoreAll();
  });

  it('should return verification error if input is not verified', async () => {
    const fakeContainer = {
      items: {
        create: async (_: any) => {
          throw new Error('Create method not implemented');
        },
      },
    } as unknown as Container;

    const mVerify = mock.method(Verify, 'inputVerified').mock;
    mVerify.mockImplementation(() => false);

    const mGetUniqueId = mock.method(CosmosConnector, 'getUniqueId').mock;
    mGetUniqueId.mockImplementation(() => 'unique-id');

    const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;

    // Arrange: wrong shape of document on purpose.
    const doc = { name: 'test' } as unknown as RawInput;

    // Act:
    const insertDocumentResult = await insertDocument(fakeContainer, doc);

    // Assert - State verification.
    if (isVerificationErrors(insertDocumentResult)) {
      assert.deepStrictEqual(insertDocumentResult, {
        message: 'Verification failed',
      });
    } else {
      throw new Error('Result is not of type VerificationErrors');
    }

    // Assert - Behavior verification: Verify that create was never called.
    assert.strictEqual(mContainerCreate.callCount(), 0);
  });
  it('should insert document successfully', async () => {
    // Arrange: override inputVerified to return true.
    const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
      createTestInputAndResult();

    const fakeContainer = {
      items: {
        create: async (doc: any) => {
          return { resource: result };
        },
      },
    } as unknown as Container;

    const mVerify = mock.method(Verify, 'inputVerified').mock;
    mVerify.mockImplementation(() => true);

    const mContainerCreate = mock.method(
      fakeContainer.items as any,
      'create',
    ).mock;
    mContainerCreate.mockImplementation(async (doc: any) => {
      return { resource: result };
    });

    // Act:
    const receivedResult = await insertDocument(fakeContainer, input);

    // Assert - State verification: Ensure the result is as expected.
    assert.deepStrictEqual(receivedResult, result);

    // Assert - Behavior verification: Ensure create was called once with correct arguments.
    assert.strictEqual(mContainerCreate.callCount(), 1);
    assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
      id: input.id,
      name: result.name,
    });
  });
  it('should return error if db insert fails', async () => {
    // Arrange: override inputVerified to return true.
    const { input, result } = createTestInputAndResult();
    const errorMessage: string = 'An unknown error occurred';

    const fakeContainer = {
      items: {
        create: async (doc: any): Promise<any> => {
          return Promise.resolve(null);
        },
      },
    } as unknown as Container;

    const mVerify = mock.method(Verify, 'inputVerified').mock;
    mVerify.mockImplementation(() => true);

    const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;
    mContainerCreate.mockImplementation(async (doc: any) => {
      const mockError: DbError = {
        message: errorMessage,
        code: 500,
      };
      throw mockError;
    });

    // Act:
    const insertDocumentResult = await insertDocument(fakeContainer, input);

    // // Assert - Ensure create method was called once with the correct arguments.
    assert.strictEqual(isDbError(insertDocumentResult), true);
    assert.strictEqual(mContainerCreate.callCount(), 1);
    assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
      id: input.id,
      name: result.name,
    });
  });
});

Résolution des problèmes

La plupart du code de cet article provient du dépôt GitHub MicrosoftDocs/node-essentials . Si vous souhaitez insérer dans une ressource Cosmos DB Cloud, créez la ressource avec ce script.

Informations supplémentaires