Partager via


Tester l’intégration du SDK Azure dans les 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.

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.

Prérequis

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 mocks peuvent diverger du SDK réel, entraînant des écarts.
  • 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 :

  • Fournit un environnement réaliste qui reflète de près la production.
  • Utile pour les tests d’intégration pour s’assurer que différentes parties du système fonctionnent ensemble.
  • Aide à identifier les problèmes liés à la fiabilité du réseau, à la disponibilité du service et à la gestion des données réelles.

Inconvénients :

  • Plus lent en raison des appels réseau.
  • Plus coûteux en raison des coûts potentiels d’utilisation des services.
  • Complexe et long à configurer et à maintenir un environnement de service réel correspondant à 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 : c’est la 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 : c’est un mock de la dépendance.
// setup
const dependencyFunctionMock = jest.fn();

// perform test
// Jest replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// verify behavior
expect(dependencyFunctionMock).toHaveBeenCalled();

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.

Stubs

L’objectif des stubs est de remplacer les données de retour d’une fonction pour simuler différents scénarios. Cela permet à votre code d’appeler la fonction et de recevoir divers états, y compris des résultats réussis, des échecs, des exceptions et des cas limites. La vérification de l’état garantit que votre code gère correctement ces scénarios.

// ARRANGE
const dependencyFunctionMock = jest.fn();
const fakeDatabaseData = {first: 'John', last: 'Jones'};
dependencyFunctionMock.mockReturnValue(fakeDatabaseData);

// ACT
// date is returned by mock then transformed in SomeTestFunction()
const { name } = someTestFunction()

// ASSERT
expect(name).toBe(`${first} ${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.

Fakes

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
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);
}

// Jest test suite
describe('someTestFunction', () => {
  let fakeDb: FakeDatabase;
  let testKey: string;
  let testValue: any;

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

    // Spy on the save method
    jest.spyOn(fakeDb, 'save');
  });

  afterEach(() => {
    // Clear all mocks
    jest.clearAllMocks();
  });

  test('should save and return the correct value', () => {
    // Perform test
    const result = someTestFunction(fakeDb, testKey, testValue);

    // Verify state
    expect(result).toEqual(testValue);
    expect(result.first).toBe('John');
    expect(result.last).toBe('Jones');
    expect(result.lastUpdated).toBe(testValue.lastUpdated);

    // Verify behavior
    expect(fakeDb.save).toHaveBeenCalledWith(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 en utilisant le 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';
import {
  DbDocument,
  DbError,
  RawInput,
  VerificationErrors,
} from '../data/model';
import { inputVerified } from '../data/verify';

export async function insertDocument(
  container: Container,
  doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
  const isVerified: boolean = 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 pour écrire du JavaScript sûr en termes de types.

Les fonctions de cette application sont :

Fonction Description
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

Cet article utilise Jest comme framework de test. Il existe d’autres frameworks de test comparables que vous pouvez également utiliser.

À la racine du répertoire de l’application, installez Jest avec la commande suivante :

npm install jest

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. Jest recherche des fichiers suivant la convention de nommage courante pour les fichiers de test : <file-name>.spec.[jt]s. Ce modèle signifie que les fichiers nommés comme les exemples suivants seront interprétés comme des fichiers de test et exécutés par Jest :

  • *.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 Jest :

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

Le code source TypeScript est généré dans le sous-dossier dist. Jest exécute les fichiers .spec.js trouvés dans le sous-dossier dist.

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 vocabulaire de test, cela est connu sous le nom de :

  • 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.

Jest, à l’instar d’autres frameworks de test, possède un modèle de fichier de test pour définir votre fichier de test.

// boilerplate.spec.ts

describe('nameOfGroupOfTests', () => {
  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
  });
});

Lors de l’utilisation de mocks, ce modèle doit utiliser des mocks 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 mocks pour simuler un appel à une dépendance comporte quelques configurations supplémentaires en plus du code de modèle de fichier de test courant. Il y a plusieurs parties au fichier de test ci-dessous :

  • import : les instructions d’importation vous permettent d’utiliser ou de simuler n’importe lequel de vos tests.
  • jest.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.
  • test: 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 Test
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 Jest suivant montre comment tester la fonction insertDocument.

// insertDocument.test.ts
import { Container } from '../data/connect-to-cosmos';
import { createTestInputAndResult } from '../data/fake-data';
import {
  DbDocument,
  DbError,
  isDbError,
  isVerificationErrors,
  RawInput,
} from '../data/model';
import { inputVerified } from '../data/verify';
import { insertDocument } from './insert';

// Mock app dependencies for Cosmos DB setup
jest.mock('../data/connect-to-cosmos', () => ({
  connectToContainer: jest.fn(),
  getUniqueId: jest.fn().mockReturnValue('unique-id'),
}));

// Mock app dependencies for input verification
jest.mock('../data/verify', () => ({
  inputVerified: jest.fn(),
}));

describe('insertDocument', () => {
  // Mock the Cosmo DB Container object
  let mockContainer: jest.Mocked<Container>;

  beforeEach(() => {
    // Clear all mocks before each test
    jest.clearAllMocks();

    // Mock the Cosmos DB Container create method
    mockContainer = {
      items: {
        create: jest.fn(),
      },
    } as unknown as jest.Mocked<Container>;
  });

  it('should return verification error if input is not verified', async () => {
    // Arrange - Mock the input verification function to return false
    jest.mocked(inputVerified).mockReturnValue(false);

    // Arrange - wrong shape of doc on purpose
    const doc = { name: 'test' };

    // Act - Call the function to test
    const insertDocumentResult = await insertDocument(
      mockContainer,
      doc as unknown as RawInput,
    );

    // Assert - State verification: Check the result when verification fails
    if (isVerificationErrors(insertDocumentResult)) {
      expect(insertDocumentResult).toEqual({
        message: 'Verification failed',
      });
    } else {
      throw new Error('Result is not of type VerificationErrors');
    }

    // Assert - Behavior verification: Ensure create method was not called
    expect(mockContainer.items.create).not.toHaveBeenCalled();
  });

  it('should insert document successfully', async () => {
    // Arrange - create input and expected result data
    const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
      createTestInputAndResult();

    // Arrange - mock the input verification function to return true
    (inputVerified as jest.Mock).mockReturnValue(true);
    (mockContainer.items.create as jest.Mock).mockResolvedValue({
      resource: result,
    });

    // Act - Call the function to test
    const insertDocumentResult = await insertDocument(mockContainer, input);

    // Assert - State verification: Check the result when insertion is successful
    expect(insertDocumentResult).toEqual(result);

    // Assert - Behavior verification: Ensure create method was called with correct arguments
    expect(mockContainer.items.create).toHaveBeenCalledTimes(1);
    expect(mockContainer.items.create).toHaveBeenCalledWith({
      id: input.id,
      name: result.name,
    });
  });

  it('should return error if db insert fails', async () => {
    // Arrange - create input and expected result data
    const { input, result } = createTestInputAndResult();

    // Arrange - mock the input verification function to return true
    jest.mocked(inputVerified).mockReturnValue(true);

    // Arrange - mock the Cosmos DB create method to throw an error
    const mockError: DbError = {
      message: 'An unknown error occurred',
      code: 500,
    };
    jest.mocked(mockContainer.items.create).mockRejectedValue(mockError);

    // Act - Call the function to test
    const insertDocumentResult = await insertDocument(mockContainer, input);

    // Assert - verify type as DbError
    if (isDbError(insertDocumentResult)) {
      expect(insertDocumentResult.message).toBe(mockError.message);
    } else {
      throw new Error('Result is not of type DbError');
    }

    // Assert - Behavior verification: Ensure create method was called with correct arguments
    expect(mockContainer.items.create).toHaveBeenCalledTimes(1);
    expect(mockContainer.items.create).toHaveBeenCalledWith({
      id: input.id,
      name: result.name,
    });
  });
});

Informations supplémentaires