Condividi tramite


Test dell'integrazione di Azure SDK nelle applicazioni JavaScript

Il test del codice di integrazione per Azure SDK per JavaScript è essenziale per garantire che le applicazioni interagiscano correttamente con i servizi di Azure.

Quando si decide se simulare le chiamate dell'SDK del servizio cloud o usare un servizio live a scopo di test, è importante considerare i compromessi tra velocità, affidabilità e costi.

Prerequisiti

Simulazione dei servizi cloud

Vantaggi:

  • Accelera il gruppo di test eliminando la latenza di rete.
  • Fornisce ambienti di test prevedibili e controllati.
  • Più facile simulare vari scenari e casi perimetrali.
  • Riduce i costi associati all'uso di servizi cloud live, in particolare nelle pipeline di integrazione continua.

Svantaggi:

  • Le simulazioni possono derivare dall'SDK effettivo, causando discrepanze.
  • Potrebbe ignorare determinate funzionalità o comportamenti del servizio live.
  • Ambiente meno realistico rispetto alla produzione.

Uso di un servizio attivo

Vantaggi:

  • Fornisce un ambiente realistico che rispecchia attentamente la produzione.
  • Utile per i test di integrazione per garantire che parti diverse del sistema funzionino insieme.
  • Consente di identificare i problemi relativi all'affidabilità della rete, alla disponibilità del servizio e alla gestione effettiva dei dati.

Svantaggi:

  • Più lento a causa delle chiamate di rete.
  • Più costoso a causa di potenziali costi di utilizzo del servizio.
  • Complesso e dispendioso in termini di tempo per configurare e gestire un ambiente di servizio live che corrisponda alla produzione.

La scelta tra simulazione e uso di servizi live dipende dalla strategia di test. Per gli unit test in cui la velocità e il controllo sono fondamentali, la simulazione è spesso la scelta migliore. Per i test di integrazione in cui il realismo è fondamentale, l'uso di un servizio live può fornire risultati più accurati. Il bilanciamento di questi approcci consente di ottenere una copertura completa dei test, gestendo i costi e mantenendo l'efficienza dei test.

Test doubles: Mocks, stub e fakes

Un test double è qualsiasi tipo di sostituzione usato al posto di qualcosa di reale a scopo di test. Il tipo di double scelto è basato su ciò che si vuole sostituire. Il termine mock è spesso inteso come qualsiasi doppio quando il termine viene usato casualmente. In questo articolo il termine viene usato in modo specifico e illustrato in modo specifico nel framework di test Jest.

Simulazioni

Mocks (detto anche spies): sostituire in una funzione e essere in grado di controllare e spiare il comportamento di tale funzione quando viene chiamato indirettamente da un altro codice.

Negli esempi seguenti sono disponibili 2 funzioni:

  • someTestFunction: questa è la funzione che è necessario testare. Chiama una dipendenza, dependencyFunction, che non è stata scritta e non è necessario testare.
  • dependencyFunctionMock: si tratta di una simulazione della dipendenza.
// setup
const dependencyFunctionMock = jest.fn();

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

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

Lo scopo del test è garantire che alcuniTestFunction si comportino correttamente senza richiamare effettivamente il codice di dipendenza. Il test verifica che sia stata chiamata la simulazione della dipendenza.

Simulare dipendenze di grandi dimensioni e piccole

Quando si decide di simulare una dipendenza, è possibile scegliere di simulare solo ciò che è necessario, ad esempio:

  • Una funzione o due da una dipendenza più grande. Jest offre simulazioni parziali a questo scopo.
  • Tutte le funzioni di una dipendenza più piccola, come illustrato nell'esempio in questo articolo.

Stub

Lo scopo degli stub consiste nel sostituire i dati restituiti di una funzione per simulare scenari diversi. In questo modo il codice può chiamare la funzione e ricevere vari stati, inclusi risultati riusciti, errori, eccezioni e casi perimetrali. La verifica dello stato garantisce che il codice gestisca correttamente questi scenari.

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

Lo scopo del test precedente è garantire che il lavoro svolto da someTestFunction soddisfi il risultato previsto. In questo semplice esempio, l'attività della funzione consiste nel concatenare i nomi di prima e famiglia. Usando dati falsi, si conosce il risultato previsto e si può verificare che la funzione esegua correttamente il lavoro.

Fakes

Fakes sostituisce una funzionalità che normalmente non viene usata nell'ambiente di produzione, ad esempio l'uso di un database in memoria anziché un database 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);
  });
});

Lo scopo del test precedente è garantire che someTestFunction interagisca correttamente con il database. Usando un database fittizio in memoria, è possibile testare la logica della funzione senza basarsi su un database reale, rendendo i test più veloci e affidabili.

Scenario: inserimento di un documento in Cosmos DB con Azure SDK

Si supponga di avere un'applicazione che deve scrivere un nuovo documento in Cosmos DB se tutte le informazioni vengono inviate e verificate. Se viene inviato un modulo vuoto o le informazioni non corrispondono al formato previsto, l'applicazione non deve immettere i dati.

Cosmos DB viene usato come esempio, ma i concetti si applicano alla maggior parte degli SDK di Azure per JavaScript. La funzione seguente acquisisce questa funzionalità:

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

Nota

I tipi TypeScript consentono di definire i tipi di dati usati da una funzione. Anche se non è necessario TypeScript per usare Jest o altri framework di test JavaScript, è essenziale per la scrittura di JavaScript indipendenti dai tipi.

Le funzioni in questa applicazione precedente sono:

Funzione Descrizione
insertDocument Inserisce un documento nel database. Questo è ciò che vogliamo testare.
inputVerified Verifica i dati di input rispetto a uno schema. Assicura che i dati siano nel formato corretto(ad esempio, indirizzi di posta elettronica validi, URL formattati correttamente).
cosmos.items.create Funzione SDK per Azure Cosmos DB usando il @azure/cosmos. Questo è ciò che vogliamo simulare. Ha già i propri test gestiti dai proprietari del pacchetto. È necessario verificare che la chiamata di funzione di Cosmos DB sia stata effettuata e restituita dati se i dati in ingresso hanno superato la verifica.

Installare la dipendenza del framework di test

Questo articolo usa Jest come framework di test. Esistono anche altri framework di test, che sono paragonabili.

Nella radice della directory dell'applicazione installare Jest con il comando seguente:

npm install jest

Configurare il pacchetto per l'esecuzione del test

Aggiornare per package.json l'applicazione con un nuovo script per testare i file del codice sorgente. I file di codice sorgente vengono definiti dalla corrispondenza in base al nome e all'estensione di file parziali. Jest cerca i file che seguono la convenzione di denominazione comune per i file di test: <file-name>.spec.[jt]s. Questo modello indica che i file denominati come gli esempi seguenti verranno interpretati come file di test ed eseguiti da Jest:

  • *.test.js: ad esempio, math.test.js
  • *.spec.js: ad esempio, math.spec.js
  • File che si trovano in una directory di test, ad esempio test/math.js

Aggiungere uno script al package.json per supportare il modello di file di test con Jest:

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

Il codice sorgente TypeScript viene generato nella dist sottocartella. Jest esegue i .spec.js file trovati nella dist sottocartella.

Configurare unit test per Azure SDK

Come è possibile usare mocks, stub e fakes per testare la funzione insertDocument ?

  • Simulazioni: è necessario un mock per assicurarsi che il comportamento della funzione sia testato, ad esempio:
    • Se i dati superano la verifica, la chiamata alla funzione Cosmos DB è avvenuta solo 1 volta
    • Se i dati non superano la verifica, la chiamata alla funzione Cosmos DB non è stata eseguita
  • Stub:
    • I dati passati corrispondono al nuovo documento restituito dalla funzione .

Quando si esegue il test, considerare in termini di configurazione del test, il test stesso e la verifica. In termini di test gerculare, questo è noto come:

  • Disporre: configurare le condizioni di test
  • Azione: chiamare la funzione da testare, nota anche come sistema sottoposto a test o SUT
  • Assert: convalidare i risultati. I risultati possono essere di comportamento o di stato.
    • Il comportamento indica la funzionalità nella funzione di test, che può essere verificata. Un esempio è che è stata chiamata una certa dipendenza.
    • Lo stato indica i dati restituiti dalla funzione.

Jest, simile ad altri framework di test, ha il file boilerplate di test per definire il file di 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
  });
});

Quando si usano simulazioni, tale posizione della caldaia deve usare il mocking per testare la funzione senza chiamare la dipendenza sottostante usata nella funzione, ad esempio le librerie client di Azure.

Creare il file di test

Il file di test con simulazioni per simulare una chiamata a una dipendenza include alcune configurazioni aggiuntive aggiuntive per il codice boilerplate di test comune. Di seguito sono riportate diverse parti del file di test:

  • import: le istruzioni import consentono di usare o simulare uno dei test.
  • jest.mock: creare il comportamento fittizio predefinito desiderato. Ogni test può modificare in base alle esigenze.
  • describe: gruppo di test per il insert.ts file.
  • test: ogni test per il insert.ts file.

Il file di test illustra tre test per il insert.ts file, che possono essere suddivisi in due tipi di convalida:

Validation type (Tipo di convalida)   Test
Percorso felice: should insert document successfully Il metodo di database fittizio è stato chiamato e ha restituito i dati modificati.
Percorso errore: should return verification error if input is not verified La convalida dei dati non è riuscita e ha restituito un errore.
Percorso errore:should return error if db insert fails Il metodo di database fittizio è stato chiamato e ha restituito un errore.

Il file di test Jest seguente illustra come testare la funzione 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,
    });
  });
});

Informazioni aggiuntive