Freigeben über


So testen Sie die Azure SDK-Integration in JavaScript-Anwendungen

Das Testen Ihres Integrationscodes für das Azure SDK für JavaScript ist unerlässlich, um sicherzustellen, dass Ihre Anwendungen ordnungsgemäß mit Azure-Diensten interagieren. In diesem Handbuch erfahren Sie, wie Sie die Azure SDK-Integration in Ihre JavaScript-Anwendungen effektiv testen, ein Testframework.

Bei der Entscheidung, ob Clouddienst SDK-Aufrufe simuliert oder ein Livedienst zu Testzwecken verwendet werden soll, ist es wichtig, die Kompromisse zwischen Geschwindigkeit, Zuverlässigkeit und Kosten zu berücksichtigen. In diesem Artikel wird veranschaulicht, wie Sie ein Testframework zum Testen der SDK-Integration verwenden. Der Anwendungscode fügt ein Dokument in Cosmos DB ein. Der Testcode simuliert die Ressourcennutzung, sodass die Cloudressource nicht verwendet wird.

Die verwendeten Frameworks sind:

  • Jest mit CommonJs
  • Vitest mit ESM
  • Node.js Testrunner mit ESM

Voraussetzungen

Node.js LTS. LTS-Veröffentlichungsstatus ist "langfristiger Support", der in der Regel garantiert, dass kritische Fehler für insgesamt 30 Monate behoben werden.

Der Node.js Test-Runner ist Teil der Node.js Installation.

Vorsicht

Das für den Node.js-Testläufer bereitgestellte Beispiel verwendet das experimentelle node:test-Modul mit mock.fn(). Denken Sie daran, dass der eingebaute Test-Runner von Node noch keine vollständig unterstützte Mocking-API bietet. Stellen Sie sicher, dass Ihre Zielknotenversion die experimentellen APIs unterstützt, oder erwägen Sie stattdessen die Verwendung einer Pseudobibliothek (oder Stubfunktionen von Drittanbietern).

Simulierte Clouddienste

Vorteile:

  • Beschleunigt die Testsuite durch Eliminierung der Netzwerklatenz.
  • Stellt vorhersehbare und kontrollierte Testumgebungen bereit.
  • Einfachere Simulation verschiedener Szenarien und Randfälle.
  • Reduziert die Kosten für die Verwendung von Live-Clouddiensten, insbesondere in kontinuierlichen Integrationspipelines.

Nachteile:

  • Mocks können vom eigentlichen SDK abweichen, was zu Unstimmigkeiten führen kann.
  • Möglicherweise werden bestimmte Features oder Verhaltensweisen des Livediensts ignoriert.
  • Weniger realistische Umgebung im Vergleich zur Produktion.

Verwenden eines Livediensts

Vorteile:

  • Ist ein realistisches Umfeld, das die Produktion eng widerspiegelt?
  • Ist nützlich für Integrationstests, um sicherzustellen, dass verschiedene Teile des Systems zusammenarbeiten?
  • Ist hilfreich, um Probleme im Zusammenhang mit der Netzwerkzuverlässigkeit, der Dienstverfügbarkeit und der tatsächlichen Datenverarbeitung zu identifizieren?

Nachteile:

  • Ist aufgrund von Netzwerkanrufen langsamer.
  • Ist aufgrund potenzieller Dienstnutzungskosten teurer.
  • Ist komplex und zeitaufwändig, um eine Live-Dienstumgebung einzurichten und aufrechtzuerhalten, die der Produktion entspricht.

Die Wahl zwischen Mocking und Nutzung von Livediensten hängt von Ihrer Teststrategie ab. Bei Komponententests, bei denen Geschwindigkeit und Kontrolle von größter Bedeutung sind, ist Mocking oft die bessere Wahl. Bei Integrationstests, bei denen Realismus entscheidend ist, kann die Verwendung eines Livedienstes genauere Ergebnisse liefern. Das Ausgleichen dieser Ansätze hilft dabei, eine umfassende Testabdeckung zu erzielen und gleichzeitig Kosten zu verwalten und die Testeffizienz aufrechtzuerhalten.

Testdoubles: Pseudodaten, Stubs und Fälschungen

Ein Testdouble ist jede Art von Ersatz, der anstelle von etwas Realem zu Testzwecken verwendet wird. Die Art des Doubles, das Sie auswählen, hängt davon ab, was Sie ersetzen möchten. Der Begriff Simulation meint häufig einen Double, wenn der Begriff beiläufig verwendet wird. In diesem Artikel wird der Begriff gezielt verwendet und speziell im Jest-Test-Framework veranschaulicht.

Mock-Objekte

Mocks (auch als Spies bezeichnet): Können eine Funktion ersetzen und das Verhalten dieser Funktion steuern und ausspähen, wenn sie indirekt von einem anderen Code aufgerufen wird.

In den folgenden Beispielen geht es um zwei Funktionen:

  • someTestFunction: Die Funktion, die Sie testen müssen. Sie ruft die Abhängigkeit dependencyFunction auf, die Sie nicht geschrieben haben und nicht testen müssen.
  • dependencyFunctionMock: Mock of the dependency.
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);

Der Test dient dazu, sicherzustellen, dass sich someTestFunction ordnungsgemäß verhält, ohne tatsächlich den Abhängigkeitscode aufrufen zu müssen. Der Test überprüft, ob der Mock der Abhängigkeit aufgerufen wurde.

Mocking großer und kleiner Abhängigkeiten

Wenn Sie sich entscheiden, eine Abhängigkeit zu simulieren, können Sie auswählen, nur das zu simulieren, was Sie benötigen, wie zum Beispiel:

  • Eine oder zwei Funktionen aus einer größeren Abhängigkeit. Jest bietet zu diesem Zweck TeilMocks.
  • Alle Funktionen einer kleineren Abhängigkeit, wie im Beispiel in diesem Artikel gezeigt.

Stümpfe

Der Zweck eines Stubs besteht darin, die Rückgabedaten einer Funktion zu ersetzen, um verschiedene Szenarien zu simulieren. Sie verwenden einen Stub, damit Ihr Code die Funktion aufrufen und verschiedene Zustände empfangen kann, einschließlich erfolgreicher Ergebnisse, Fehler, Ausnahmen und Randfälle. Die Zustandsüberprüfung stellt sicher, dass Ihr Code diese Szenarien ordnungsgemäß verarbeitet.

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

Der Zweck des vorherigen Tests besteht darin, sicherzustellen, dass durch die von someTestFunction ausgeführte Aufgabe das erwartete Ergebnis erreicht wird. In diesem einfachen Beispiel besteht die Aufgabe der Funktion darin, die Vor- und Familiennamen zu verketten. Durch die Verwendung von Fakedaten kennen Sie das erwartete Ergebnis und können überprüfen, ob die Funktion die Aufgabe korrekt ausführt.

Fälschungen

Fakes ersetzen eine Funktionalität, die Sie normalerweise nicht in der Produktion verwenden würden, wie z. B. die Verwendung einer In-Memory-Datenbank anstelle einer Clouddatenbank.

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

Der Zweck des vorherigen Tests besteht darin, sicherzustellen, dass someTestFunction ordnungsgemäß mit der Datenbank interagiert. Durch die Verwendung einer gefälschten In-Memory-Datenbank können Sie die Logik der Funktion testen, ohne auf eine echte Datenbank angewiesen zu sein, wodurch die Tests schneller und zuverlässiger werden.

Szenario: Einfügen eines Dokuments in Cosmos DB mithilfe des Azure SDK

Angenommen, Sie haben eine Anwendung, die ein neues Dokument in Cosmos DB schreiben muss, wenn alle Informationen übermittelt und überprüft werden. Wenn ein leeres Formular übermittelt wird oder die Informationen nicht mit dem erwarteten Format übereinstimmen, sollte die Anwendung die Daten nicht eingeben.

Cosmos DB wird als Beispiel verwendet, die Konzepte gelten jedoch für die meisten Azure SDKs für JavaScript. Die folgende Funktion erfasst diese Funktionalität:

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

Hinweis

TypeScript-Typen helfen beim Definieren der Arten von Daten, die eine Funktion verwendet. Während Sie TypeScript nicht benötigen, um Jest oder andere JavaScript-Testframeworks zu verwenden, ist es für das Schreiben von typsicherem JavaScript unerlässlich.

Die Funktionen in dieser Anwendung sind:

Funktion Beschreibung
insertDocument Fügt ein Dokument in die Datenbank ein. Das soll getestet werden.
inputVerified Überprüft die Eingabedaten anhand eines Schemas. Stellt sicher, dass Daten im richtigen Format vorliegen (z. B. gültige E-Mail-Adressen, korrekt formatierte URLs).
cosmos.items.create SDK-Funktion für Azure Cosmos DB mithilfe von @azure/cosmos. Das wollen wir nachahmen. Es hat bereits eigene Tests, die von den Paketverwaltern gepflegt werden. Wir müssen überprüfen, ob der Cosmos DB-Funktionsaufruf erfolgt ist und Daten zurückgegeben wurden, wenn die eingehenden Daten die Überprüfung bestanden haben.

Installieren der Testframeworkabhängigkeit

Dieses Framework wird als Teil Node.js LTS bereitgestellt.

Konfigurieren des Pakets zum Ausführen des Tests

Aktualisieren Sie package.json für die Anwendung mit einem neuen Skript, um unsere Quellcodedateien zu testen. Quellcode-Dateien werden durch den Abgleich eines Teil-Dateinamens und der Dateierweiterung definiert. Testrunner sucht nach Dateien, die auf die allgemeine Benennungskonvention für Testdateien folgen: <file-name>.spec.[jt]s. Dieses Muster bedeutet, dass Dateien, die wie die folgenden Beispiele benannt sind, als Testdateien interpretiert und von Testrunner ausgeführt werden:

  • * .test.js: Beispiel: math.test.js
  • * .spec.js: Beispiel: math.spec.js
  • Dateien, die sich im Verzeichnis tests befinden, wie tests/math.js

Fügen Sie der package.json ein Skript hinzu, um dieses Testdateimuster mit Test runner zu unterstützen:

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

Einrichten des Komponententests für das Azure SDK

Wie können wir Mocks, Stubs und Fakes verwenden, um die insertDocument-Funktion zu testen?

  • Mocks: Wir benötigen eine Simulation, um sicherzustellen, dass das Verhalten der Funktion getestet wird. Beispiel:
    • Wenn die Daten die Überprüfung bestehen, ist der Aufruf der Cosmos DB-Funktion nur einmal erfolgt.
    • Wenn die Daten die Überprüfung nicht bestehen, ist kein Aufruf der Cosmos DB-Funktion erfolgt.
  • Stümpfe:
    • Die übergebenen Daten entsprechen dem neuen Dokument, das von der Funktion zurückgegeben wird.

Denken Sie beim Testen an den Testaufbau, den Test selbst und die Verifizierung. Im Hinblick auf den Testjargon verwendet diese Funktion die folgenden Begriffe:

  • Anordnen: richten Sie die Testbedingungen ein
  • Handeln: Rufen Sie die zu testende Funktion auf, auch bekannt als system under test oder SUT
  • Bestätigen: überprüfen Sie die Ergebnisse. Ergebnisse können Verhalten oder Zustand sein.
    • Das Verhalten gibt die Funktionalität in Ihrer Testfunktion an, die überprüft werden kann. Ein Beispiel dafür ist, dass einige Abhängigkeiten aufgerufen wurden.
    • State gibt die von der Funktion zurückgegebenen Daten an.
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
  });
});

Wenn Sie Mocks in Ihren Tests verwenden, muss dieser Vorlagencode Mocking verwenden, um die Funktion zu testen, ohne die zugrunde liegende Abhängigkeit in der Funktion aufzurufen, z. B. die Azure-Clientbibliotheken.

Erstellen der Testdatei

Die Testdatei mit Mocks, um einen Aufruf einer Abhängigkeit zu simulieren, verfügt über ein zusätzliches Setup.

Es gibt mehrere Teile der Testdatei:

  • import: Mit den Importanweisungen können Sie einen Ihrer Tests verwenden oder simulieren.
  • mock: Erstellen Sie das gewünschte standardmäßige Scheinverhalten. Jeder Test kann bei Bedarf geändert werden.
  • describe: Testgruppenfamilie für die insert.ts-Datei.
  • it: Jeder Test für die insert.ts-Datei.

Die Testdatei umfasst drei Tests für die insert.ts-Datei, die in zwei Überprüfungstypen unterteilt werden kann:

Überprüfungstyp Testen
Positiver Pfad: should insert document successfully Die simulierte Datenbankmethode wurde aufgerufen und hat geänderte Daten zurückgegeben.
Fehlerpfad: should return verification error if input is not verified Die Validierung der Daten schlug fehl und lieferte einen Fehler zurück.
Fehlerpfad: should return error if db insert fails Die simulierte Datenbankmethode wurde aufgerufen und hat einen Fehler zurückgegeben.

Die folgende Testdatei zeigt, wie die insertDocument-Funktion getestet wird.

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

Problembehandlung

Der großteil des Codes in diesem Artikel stammt aus dem GitHub-Repository "MicrosoftDocs/node-essentials ". Wenn Sie in eine Cosmos DB Cloud-Ressource einfügen möchten, erstellen Sie die Ressource mit diesem Skript.

Weitere Informationen