Delen via


Azure SDK-integratie testen in JavaScript-toepassingen

Het testen van uw integratiecode voor de Azure SDK voor JavaScript is essentieel om ervoor te zorgen dat uw toepassingen correct communiceren met Azure-services. In deze handleiding leert u hoe u azure SDK-integratie effectief kunt testen in uw JavaScript-toepassingen als testframework.

Bij het bepalen of u cloudservice-SDK-aanroepen wilt nadoen of een liveservice wilt gebruiken voor testdoeleinden, is het belangrijk om rekening te houden met de afwegingen tussen snelheid, betrouwbaarheid en kosten. In dit artikel wordt beschreven hoe u een testframework gebruikt voor het testen van SDK-integratie. Met de toepassingscode wordt een document ingevoegd in Cosmos DB. Met de testcode wordt dat resourcegebruik gesimuleerd, zodat de cloudresource niet wordt gebruikt.

De gebruikte frameworks zijn:

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

Vereiste voorwaarden

Node.js LTS. De releasestatus van LTS is 'langetermijnondersteuning', wat doorgaans garandeert dat kritieke bugs in totaal 30 maanden worden opgelost.

De Node.js testloper maakt deel uit van de Node.js installatie.

Waarschuwing

Het voorbeeld voor de Node.js testrunner maakt gebruik van de experimentele module node:test met mock.fn(). Houd er rekening mee dat de ingebouwde testrunner van Node nog geen volledig ondersteunde mocking-API biedt. Zorg ervoor dat uw Node-versie de experimentele API's ondersteunt of overweeg in plaats daarvan een externe mock-bibliotheek (of stub-functies) te gebruiken.

Het simuleren van clouddiensten

Voordelen:

  • Versnelt het testpakket door netwerklatentie te elimineren.
  • Biedt voorspelbare en gecontroleerde testomgevingen.
  • Eenvoudiger om verschillende scenario's en edge-cases te simuleren.
  • Verlaagt de kosten voor het gebruik van live cloudservices, met name in pijplijnen voor continue integratie.

nadelen:

  • Mocks kunnen afwijken van de werkelijke SDK, wat leidt tot discrepanties.
  • Bepaalde functies of gedragingen van de liveservice kunnen worden genegeerd.
  • Minder realistische omgeving in vergelijking met productie.

Een liveservice gebruiken

Voordelen:

  • Is een realistische omgeving die de productie nauw weerspiegelt?
  • Is het handig voor integratietests om ervoor te zorgen dat verschillende onderdelen van het systeem samenwerken?
  • Is het handig om problemen te identificeren met betrekking tot netwerkbetrouwbaarheid, servicebeschikbaarheid en werkelijke gegevensverwerking?

nadelen:

  • Is langzamer vanwege netwerkaanroepen.
  • Is duurder vanwege mogelijke servicegebruikskosten.
  • Is complex en tijdrovend om een liveserviceomgeving in te stellen en te onderhouden die overeenkomt met de productie.

De keuze tussen mocking en het gebruik van liveservices is afhankelijk van uw teststrategie. Voor eenheidstests waarbij snelheid en controle van cruciaal belang zijn, is het gebruik van mocks vaak de betere keuze. Voor integratietests waarbij realisme cruciaal is, kan het gebruik van een liveservice nauwkeurigere resultaten bieden. Door deze benaderingen te verdelen, kunt u een uitgebreide testdekking bereiken terwijl u kosten beheert en de efficiëntie van de test onderhoudt.

Test doubles: Mocks, stubs en fakes

Een testdubbel is een soort vervanging die wordt gebruikt in plaats van iets echts voor testdoeleinden. Het type dubbel dat u kiest, is gebaseerd op wat u wilt vervangen. Wanneer de term mock terloops wordt gebruikt, wordt het vaak gebruikt om dubbel aan te duiden. In dit artikel wordt de term specifiek gebruikt en specifiek geïllustreerd in het Jest-testframework.

Dummy's

Mocks (ook wel spionnen genoemd): Vervang in een functie en controleer en bespioneer het gedrag van die functie wanneer deze indirect wordt aangeroepen door andere code.

In de volgende voorbeelden hebt u twee functies:

  • someTestFunction: de functie die u moet testen. Er wordt een afhankelijkheid aanroepen, dependencyFunctiondie u niet hebt geschreven en die u niet hoeft te testen.
  • dependencyFunctionMock: Mock van de afhankelijkheid.
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);

Het doel van de test is ervoor te zorgen dat sommigeTestFunction correct werkt zonder de afhankelijkheidscode daadwerkelijk aan te roepen. De test valideert dat het mock-object van de afhankelijkheid is aangeroepen.

Gesimuleerde grote versus kleine afhankelijkheden

Wanneer u besluit om een afhankelijkheid te mocken, kunt u ervoor kiezen om alleen te mocken wat u nodig hebt, zoals:

  • Een functie of twee van een grotere afhankelijkheid. Jest biedt gedeeltelijke "mocks" voor dit doel.
  • Alle functies van een kleinere afhankelijkheid, zoals wordt weergegeven in het voorbeeld in dit artikel.

Stubs

Het doel van een stub is om de retourgegevens van een functie te vervangen om verschillende scenario's te simuleren. U gebruikt een stub om uw code toe te staan om de functie aan te roepen en verschillende statussen te ontvangen, waaronder geslaagde resultaten, fouten, uitzonderingen en edge-gevallen. Statusverificatie zorgt ervoor dat uw code deze scenario's correct verwerkt.

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

Het doel van de voorgaande test is ervoor te zorgen dat het werk dat wordt uitgevoerd door someTestFunction voldoet aan het verwachte resultaat. In dit eenvoudige voorbeeld is de taak van de functie het samenvoegen van de voor- en familienamen. Door valse gegevens te gebruiken, weet u het verwachte resultaat en kunt u valideren dat de functie het werk correct uitvoert.

Vervalsingen

Fakes vervangen een functionaliteit die u normaal gesproken niet in productie zou gebruiken, zoals een in-memory database in plaats van een clouddatabase gebruiken.

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

Het doel van de voorgaande test is ervoor te zorgen dat someTestFunction de database correct communiceert. Door een valse in-memory database te gebruiken, kunt u de logica van de functie testen zonder te vertrouwen op een echte database, waardoor de tests sneller en betrouwbaarder worden.

Scenario: Document invoegen in Cosmos DB met behulp van Azure SDK

Stel dat u een toepassing hebt die een nieuw document naar Cosmos DB moet schrijven als alle informatie wordt verzonden en geverifieerd. Als er een leeg formulier wordt verzonden of de informatie niet overeenkomt met de verwachte indeling, mag de toepassing de gegevens niet invoeren.

Cosmos DB wordt als voorbeeld gebruikt, maar de concepten zijn van toepassing op de meeste Azure SDK's voor JavaScript. Met de volgende functie wordt deze functionaliteit vastgelegd:

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

Opmerking

TypeScript-typen helpen bij het definiëren van de soorten gegevens die een functie gebruikt. Hoewel u TypeScript niet nodig hebt om Jest of andere JavaScript-testframeworks te gebruiken, is het essentieel voor het schrijven van typeveilig JavaScript.

De functies in deze toepassing zijn:

Functie Beschrijving
insertDocument Hiermee voegt u een document in de database in. Dit is wat we willen testen.
inputVerified Controleert de invoergegevens op basis van een schema. Zorgt ervoor dat gegevens de juiste indeling hebben (bijvoorbeeld geldige e-mailadressen, correct opgemaakte URL's).
cosmos.items.create De SDK-functie voor Azure Cosmos DB met de @azure/cosmos. Dit is wat we willen bespotten. Het heeft al eigen tests die worden onderhouden door de pakketeigenaren. We moeten controleren of de cosmos DB-functie-aanroep is uitgevoerd en gegevens heeft geretourneerd als de binnenkomende gegevens verificatie hebben doorstaan.

Afhankelijkheid van testframework installeren

Dit framework wordt geleverd als onderdeel van Node.js LTS.

Pakket configureren om test uit te voeren

Werk de package.json toepassing bij met een nieuw script om onze broncodebestanden te testen. Broncodebestanden worden gedefinieerd op basis van afstemming op een gedeeltelijke bestandsnaam en extensie. Testrunner zoekt naar bestanden die de algemene naamconventie voor testbestanden volgen: <file-name>.spec.[jt]s. Dit patroon betekent dat bestanden met de naam zoals de volgende voorbeelden worden geïnterpreteerd als testbestanden en worden uitgevoerd door Test runner:

  • * .test.js: bijvoorbeeld math.test.js
  • * .spec.js: bijvoorbeeld math.spec.js
  • Bestanden die zich in een testmap bevinden, zoals tests/math.js

Voeg een script toe aan de package.json ter ondersteuning van dat testbestandspatroon met Test runner:

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

Eenheidstest instellen voor Azure SDK

Hoe kunnen we mocks, stubs en nepobjecten gebruiken om de insertDocument-functie te testen?

  • Mocks: we hebben een mock nodig om ervoor te zorgen dat het gedrag van de functie wordt getest, zoals:
    • Als de gegevens verificatie doorstaan, is de aanroep van de Cosmos DB-functie slechts 1 keer uitgevoerd
    • Als de gegevens niet door de verificatie komt, vindt de aanroep naar de Cosmos DB-functie niet plaats.
  • Stubs:
    • De gegevens die worden doorgegeven, komen overeen met het nieuwe document dat door de functie wordt geretourneerd.

Denk bij het testen na over de testconfiguratie, de test zelf en de verificatie. In termen van testtaal gebruikt deze functionaliteit de volgende termen:

  • Rangschikken: uw testvoorwaarden instellen
  • Act: roep uw functie aan om te testen, ook wel bekend als de testomgeving of SUT
  • Assert: valideer de resultaten. Resultaten kunnen gedrag of status zijn.
    • Gedrag geeft de functionaliteit in uw testfunctie aan, die kan worden geverifieerd. Een voorbeeld hiervan is dat er een afhankelijkheid is aangeroepen.
    • Status geeft de gegevens aan die zijn geretourneerd door de functie.
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
  });
});

Wanneer u mocks in uw tests gebruikt, moet die sjablooncode gebruik maken van mocking om de functie te testen zonder de onderliggende afhankelijkheid aan te roepen die in de functie wordt gebruikt, zoals bijvoorbeeld de Azure-clientbibliotheken.

Het testbestand maken

Het testbestand met mocks, om een aanroep naar een afhankelijkheid te simuleren, heeft een extra configuratie.

Er zijn verschillende onderdelen voor het testbestand:

  • import: met de importinstructies kunt u een van uw tests gebruiken of mocken.
  • mock: Maak het gewenste standaardsimuleerde gedrag. Elke test kan indien nodig worden gewijzigd.
  • describe: Test de groepsfamilie voor het insert.ts bestand.
  • it: Elke test voor het insert.ts bestand.

Het testbestand omvat drie tests voor het insert.ts bestand, die kunnen worden onderverdeeld in twee validatietypen:

Validatietype Testen
Gelukkig pad: should insert document successfully De gesimuleerde databasemethode is aangeroepen en heeft de gewijzigde gegevens geretourneerd.
Foutpad: should return verification error if input is not verified Validatie van gegevens is mislukt en er is een fout geretourneerd.
Errorpad:should return error if db insert fails De gesimuleerde databasemethode is aangeroepen en er is een fout geretourneerd.

In het volgende testbestand ziet u hoe u de functie insertDocument kunt testen.

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

Probleemoplossingsproces

De meeste code in dit artikel is afkomstig van de GitHub-opslagplaats MicrosoftDocs/node-essentials . Als u een Cosmos DB Cloud-resource wilt invoegen, maakt u de resource met dit script.

Aanvullende informatie