Udostępnij za pośrednictwem


Testowanie integracji zestawu Azure SDK w aplikacjach JavaScript

Testowanie kodu integracji dla zestawu Azure SDK dla języka JavaScript jest niezbędne do zapewnienia prawidłowej interakcji aplikacji z usługami platformy Azure.

Podejmując decyzję, czy wyśmiewać wywołania zestawu SDK usługi w chmurze, czy używać usługi na żywo do celów testowych, ważne jest, aby wziąć pod uwagę kompromis między szybkością, niezawodnością i kosztami.

Wymagania wstępne

Pozorowanie usług w chmurze

Zalety:

  • Przyspiesza zestaw testów, eliminując opóźnienie sieci.
  • Zapewnia przewidywalne i kontrolowane środowiska testowe.
  • Łatwiej symulować różne scenariusze i przypadki brzegowe.
  • Zmniejsza koszty związane z używaniem usług w chmurze na żywo, zwłaszcza w potokach ciągłej integracji.

Wady:

  • Makiety mogą dryfować z rzeczywistego zestawu SDK, co prowadzi do rozbieżności.
  • Może ignorować niektóre funkcje lub zachowania usługi na żywo.
  • Mniej realistyczne środowisko w porównaniu z produkcją.

Korzystanie z usługi na żywo

Zalety:

  • Zapewnia realistyczne środowisko, które ściśle odzwierciedla produkcję.
  • Przydatne w przypadku testów integracji w celu zapewnienia współpracy różnych części systemu.
  • Pomaga identyfikować problemy związane z niezawodnością sieci, dostępnością usługi i rzeczywistą obsługą danych.

Wady:

  • Wolniejsze z powodu wywołań sieciowych.
  • Droższe ze względu na potencjalne koszty użycia usługi.
  • Złożone i czasochłonne konfigurowanie i utrzymywanie środowiska usług na żywo zgodnego z produkcją.

Wybór między wyśmiewaniem i używaniem usług na żywo zależy od strategii testowania. W przypadku testów jednostkowych, w których szybkość i kontrola są najważniejsze, pozorowanie jest często lepszym wyborem. W przypadku testów integracji, w których realizm ma kluczowe znaczenie, użycie usługi na żywo może zapewnić dokładniejsze wyniki. Równoważenie tych podejść pomaga osiągnąć kompleksowy zakres testów podczas zarządzania kosztami i utrzymania wydajności testów.

Dublety testowe: Makiety, wycinki i podróbki

Test podwójny to jakikolwiek zamiennik używany zamiast czegoś rzeczywistego do celów testowych. Wybrany typ jest oparty na tym, co ma zostać zamieniony. Termin makiety jest często oznaczany jako każdy podwójny , gdy termin jest używany dorywczo. W tym artykule termin jest używany specjalnie i zilustrowany specjalnie w strukturze testowej Jest.

Makiety

Makiety (nazywane również szpiegami): Zastąp funkcję i może kontrolować i szpiegować zachowanie tej funkcji, gdy jest wywoływany pośrednio przez inny kod.

W poniższych przykładach masz 2 funkcje:

  • someTestFunction: jest to funkcja, którą należy przetestować. Wywołuje zależność , dependencyFunctionktórej nie zapisano i nie trzeba jej testować.
  • dependencyFunctionMock: jest to pozorna zależność.
// setup
const dependencyFunctionMock = jest.fn();

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

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

Celem testu jest upewnienie się, że funkcja someTestFunction działa prawidłowo bez wywoływania kodu zależności. Test sprawdza, czy wywołano pozorowanie zależności.

Pozorowanie dużych i małych zależności

Gdy zdecydujesz się wyśmiewać zależność, możesz wyśmiewać tylko to, czego potrzebujesz, takich jak:

  • Funkcja lub dwie z większej zależności. Jest oferuje częściowe makiety do tego celu.
  • Wszystkie funkcje mniejszej zależności, jak pokazano w przykładzie w tym artykule.

Klasy zastępcze

Celem wycinków jest zastąpienie zwracanych danych funkcji w celu symulowania różnych scenariuszy. Dzięki temu kod może wywoływać funkcję i odbierać różne stany, w tym pomyślne wyniki, błędy, wyjątki i przypadki brzegowe. Weryfikacja stanu gwarantuje, że kod obsługuje te scenariusze poprawnie.

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

Celem powyższego testu jest zapewnienie, że praca wykonana przez someTestFunction program spełnia oczekiwany wynik. W tym prostym przykładzie zadaniem funkcji jest połączenie imienia i nazwiska rodzin. Korzystając z fałszywych danych, znasz oczekiwany wynik i możesz sprawdzić, czy funkcja wykonuje pracę prawidłowo.

Podróbki

Podróbki zastępują funkcjonalność, której zwykle nie używa się w środowisku produkcyjnym, takich jak używanie bazy danych w pamięci zamiast bazy danych w chmurze.

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

Celem powyższego testu jest zapewnienie prawidłowej someTestFunction interakcji z bazą danych. Korzystając z fałszywej bazy danych w pamięci, można przetestować logikę funkcji bez polegania na rzeczywistej bazie danych, dzięki czemu testy będą szybsze i bardziej niezawodne.

Scenariusz: wstawianie dokumentu do usługi Cosmos DB przy użyciu zestawu Azure SDK

Załóżmy, że masz aplikację, która musi napisać nowy dokument w usłudze Cosmos DB , jeśli wszystkie informacje są przesyłane i weryfikowane. Jeśli zostanie przesłany pusty formularz lub informacje nie są zgodne z oczekiwanym formatem, aplikacja nie powinna wprowadzać danych.

Usługa Cosmos DB jest używana jako przykład, jednak koncepcje dotyczą większości zestawów SDK platformy Azure dla języka JavaScript. Poniższa funkcja przechwytuje tę funkcję:

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

Uwaga

Typy TypeScript ułatwiają definiowanie rodzajów danych używanych przez funkcję. Chociaż nie musisz używać języka TypeScript ani innych platform testowania Języka JavaScript, niezbędne jest pisanie bezpiecznego dla typu języka JavaScript.

Funkcje w tej aplikacji powyżej to:

Function opis
insertDocument Wstawia dokument do bazy danych. To jest to, co chcemy przetestować.
inputVerified Weryfikuje dane wejściowe względem schematu. Zapewnia, że dane są w poprawnym formacie (na przykład prawidłowe adresy e-mail, poprawnie sformatowane adresy URL).
cosmos.items.create Funkcja zestawu SDK dla usługi Azure Cosmos DB korzystająca z @azure/cosmos. To jest to, co chcemy wyśmiewać. Ma już własne testy obsługiwane przez właścicieli pakietów. Musimy sprawdzić, czy wywołanie funkcji usługi Cosmos DB zostało wykonane i zwróciło dane, jeśli dane przychodzące przeszły weryfikację.

Instalowanie zależności platformy testowej

W tym artykule jest używana platforma testowa Jest . Istnieją inne struktury testowe, których można również użyć.

W katalogu głównym katalogu aplikacji zainstaluj narzędzie Jest za pomocą następującego polecenia:

npm install jest

Konfigurowanie pakietu do uruchamiania testu

package.json Zaktualizuj aplikację za pomocą nowego skryptu, aby przetestować nasze pliki kodu źródłowego. Pliki kodu źródłowego są definiowane przez dopasowanie w częściowej nazwie pliku i rozszerzeniu. Jest szuka plików zgodnie ze wspólną konwencją nazewnictwa dla plików testowych: <file-name>.spec.[jt]s. Ten wzorzec oznacza, że pliki o nazwie podobnej do następujących przykładów będą interpretowane jako pliki testowe i uruchamiane przez platformę Jest:

  • *.test.js: na przykład math.test.js
  • *.spec.js: na przykład math.spec.js
  • Pliki znajdujące się w katalogu testów, takie jak testy/math.js

Dodaj skrypt do package.json , aby obsługiwać ten wzorzec pliku testowego za pomocą narzędzia Jest:

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

Kod źródłowy języka TypeScript jest generowany w dist podfolderze. Narzędzie Jest uruchamia .spec.js pliki znalezione w dist podfolderze.

Konfigurowanie testu jednostkowego dla zestawu Azure SDK

Jak możemy używać makiety, wycinków i podróbek do testowania funkcji insertDocument ?

  • Makiety: potrzebujemy makiety, aby upewnić się, że zachowanie funkcji jest testowane, takie jak:
    • Jeśli dane przechodzą weryfikację, wywołanie funkcji Cosmos DB miało miejsce tylko 1 raz
    • Jeśli dane nie przechodzą weryfikacji, wywołanie funkcji Cosmos DB nie powiodło się
  • Wycinki:
    • Przekazane dane są zgodne z nowym dokumentem zwracanym przez funkcję.

Podczas testowania zastanów się nad konfiguracją testu, samym testem i weryfikacją. Jeśli chodzi o język testowy, jest to nazywane:

  • Rozmieszczanie: konfigurowanie warunków testowych
  • Działanie: wywołaj funkcję, aby przetestować, znaną również jako system testowy lub SUT
  • Potwierdzenie: zweryfikuj wyniki. Wyniki mogą być zachowaniem lub stanem.
    • Zachowanie wskazuje funkcjonalność w funkcji testowej, którą można zweryfikować. Przykładem jest wywołanie pewnej zależności.
    • State wskazuje dane zwrócone z funkcji.

Jest, podobnie jak w innych strukturach testowych, ma standardowy plik testowy do zdefiniowania pliku testowego.

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

W przypadku korzystania z makiet, to miejsce kotła musi używać pozorowania do testowania funkcji bez wywoływania podstawowej zależności używanej w funkcji, takiej jak biblioteki klienckie platformy Azure.

Tworzenie pliku testowego

Plik testowy z makietami w celu symulowania wywołania zależności ma dodatkową konfigurację w dodatku do wspólnego kodu testowego. Poniżej znajduje się kilka części pliku testowego:

  • import: Instrukcje importowania umożliwiają używanie lub pozorowanie dowolnego testu.
  • jest.mock: Utwórz domyślne pozorne zachowanie. Każdy test może zmienić się zgodnie z potrzebami.
  • describe: Rodzina grup testowych dla insert.ts pliku.
  • test: każdy test pliku insert.ts .

Plik testowy obejmuje trzy testy dla insert.ts pliku, które można podzielić na dwa typy weryfikacji:

Typ weryfikacji Test
Szczęśliwa ścieżka: should insert document successfully Wywołano wyśmiewaną metodę bazy danych i zwrócono zmienione dane.
Ścieżka błędu: should return verification error if input is not verified Sprawdzanie poprawności danych nie powiodło się i zwróciło błąd.
Ścieżka błędu:should return error if db insert fails Wywołano wyśmiewaną metodę bazy danych i zwrócono błąd.

Poniższy plik testowy Jest pokazuje, jak przetestować funkcję 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,
    });
  });
});

Dodatkowe informacje