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.

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.

Vereisten

Cloudservices mocking

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:

  • Biedt een realistische omgeving die de productie nauw spiegelt.
  • Handig voor integratietests om ervoor te zorgen dat verschillende onderdelen van het systeem samenwerken.
  • Helpt bij het identificeren van problemen met betrekking tot netwerkbetrouwbaarheid, servicebeschikbaarheid en daadwerkelijke gegevensverwerking.

Nadelen:

  • Langzamer vanwege netwerkaanroepen.
  • Duurder vanwege mogelijke servicegebruikskosten.
  • 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 mocking 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. De term mock is vaak bedoeld als dubbel wanneer de term terloops wordt gebruikt. In dit artikel wordt de term specifiek gebruikt en specifiek geïllustreerd in het Jest-testframework.

Dummy's

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

In de volgende voorbeelden hebt u twee functies:

  • someTestFunction: dit is de functie die u moet testen. Er wordt een afhankelijkheid aanroepen, dependencyFunctiondie u niet hebt geschreven en die u niet hoeft te testen.
  • dependencyFunctionMock: dit is een mock van de afhankelijkheid.
// setup
const dependencyFunctionMock = jest.fn();

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

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

Het doel van de test is ervoor te zorgen dat sommigeTestFunction correct werkt zonder de afhankelijkheidscode daadwerkelijk aan te roepen. De test valideert dat de mock 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 stubs is het vervangen van de retourgegevens van een functie om verschillende scenario's te simuleren. Hierdoor kan uw code de functie aanroepen en verschillende statussen ontvangen, waaronder geslaagde resultaten, fouten, uitzonderingen en edge-zaken. Statusverificatie zorgt ervoor dat uw code deze scenario's correct verwerkt.

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

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.

Namaakgoederen

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

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

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: Een 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';
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;
    }
  }
}

Notitie

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 hierboven 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 behulp van 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

In dit artikel wordt Jest gebruikt als testframework. Er zijn andere testframeworks, die vergelijkbaar zijn die u ook kunt gebruiken.

Installeer Jest in de hoofdmap van de toepassingsmap met de volgende opdracht:

npm install jest

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 door te vergelijken met gedeeltelijke bestandsnaam en extensie. Jest 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 Jest:

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

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

De TypeScript-broncode wordt gegenereerd in de dist submap. Jest voert de .spec.js bestanden uit die in de dist submap zijn gevonden.

Eenheidstest instellen voor Azure SDK

Hoe kunnen we mocks, stubs en neps 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 geen verificatie doorgeven, is de aanroep naar de Cosmos DB-functie niet uitgevoerd
  • 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 staat dit bekend als:

  • Rangschikken: uw testvoorwaarden instellen
  • Act: roep uw functie aan om te testen, ook wel bekend als het systeem onder test 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.

Jest, vergelijkbaar met andere testframeworks, heeft een testbestandsplaat om uw testbestand te definiëren.

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

Wanneer u mocks gebruikt, moet die ketelplaats mocking gebruiken om de functie te testen zonder de onderliggende afhankelijkheid aan te roepen die in de functie wordt gebruikt, zoals de Azure-clientbibliotheken.

Het testbestand maken

Het testbestand met mocks om een aanroep naar een afhankelijkheid te simuleren, heeft extra instellingen naast de algemene standaardcode voor testen. Hieronder vindt u verschillende onderdelen van het testbestand:

  • import: met de importinstructies kunt u een van uw tests gebruiken of mocken.
  • jest.mock: Maak het gewenste standaardsimuleerde gedrag. Elke test kan indien nodig worden gewijzigd.
  • describe: Test de groepsfamilie voor het insert.ts bestand.
  • test: 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.
Foutpad:should return error if db insert fails De gesimuleerde databasemethode is aangeroepen en er is een fout geretourneerd.

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

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

Aanvullende informatie