Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Il test del codice di integrazione per Azure SDK per JavaScript è essenziale per garantire che le applicazioni interagiscano correttamente con i servizi di Azure. Questa guida illustra come testare in modo efficace l'integrazione di Azure SDK nelle applicazioni JavaScript un framework di test.
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. Questo articolo illustra come usare un framework di test per testare l'integrazione dell'SDK. Il codice dell'applicazione inserisce un documento in Cosmos DB. Il codice di test simula l'utilizzo delle risorse in modo che la risorsa cloud non venga usata.
I framework usati sono:
- Jest con CommonJs
- Test virtuale con ESM
- Node.js Test Runner con ESM
Prerequisiti
Node.js LTS. Lo stato di rilascio LTS è "supporto a lungo termine", che in genere garantisce che i bug critici verranno corretti per un totale di 30 mesi.
Il Node.js test runner fa parte dell'installazione di Node.js.
Attenzione
L'esempio fornito per il test runner Node.js usa il modulo experimental node:test con mock.fn(). Tieni presente che l'esecutore di test predefinito di Node non offre ancora un'API di simulazione completamente supportata. Assicurarsi che la versione del nodo di destinazione supporti le API sperimentali o valutare invece l'uso di una libreria fittizia di terze parti (o funzioni stub).
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:
- I mock possono allontanarsi dall'SDK attuale, causando discrepanze.
- Potrebbe ignorare determinate funzionalità o comportamenti del servizio live.
- Ambiente meno realistico rispetto alla produzione.
Uso di un servizio attivo
Vantaggi:
- Un ambiente realistico che rispecchia attentamente la produzione?
- È utile per i test di integrazione per garantire che diverse parti del sistema funzionino insieme?
- È utile 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 del servizio live corrispondente alla produzione.
La scelta tra simulazione (mocking) e utilizzo di servizi reali 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: Mock, stub e fake
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: funzione da testare. Chiama una dipendenza,
dependencyFunction, che non hai scritto e che non è necessario testare. - dependencyFunctionMock: Simulazione della dipendenza.
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);
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.
Segnaposti
Lo scopo di uno stub è sostituire i dati restituiti di una funzione per simulare scenari diversi. Si usa uno stub per consentire al codice di 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.
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}`);
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 il nome e il cognome. Usando dati falsi, si conosce il risultato previsto e si può verificare che la funzione esegua correttamente il lavoro.
Falsi
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
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]);
});
});
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: inserire 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.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;
}
}
}
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 scrivere JavaScript tipizzato.
Le funzioni in questa applicazione 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 prendere in giro. Ha già i propri test gestiti dai proprietari del pacchetto. È necessario verificare che sia stata effettuata la chiamata di funzione di Cosmos DB e che siano stati restituiti i dati se il dato in ingresso ha superato la verifica. |
Installare la dipendenza del framework di test
Questo framework viene fornito come parte di Node.js LTS.
Configurare il pacchetto per l'esecuzione del test
Aggiorna package.json dell'applicazione con un nuovo script per verificare i file del codice sorgente. I file di codice sorgente vengono definiti dalla corrispondenza per nome di file parziale e estensione. Test Runner 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 vengono interpretati come file di test ed eseguiti da Test Runner:
- * .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 Test Runner:
"scripts": {
"test": "node --test --experimental-test-coverage --experimental-test-module-mocks --trace-exit"
}
Configurare unit test per Azure SDK
Come è possibile usare simulazioni, stub e fittizi 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, pensare in termini di la configurazione del test, il test stesso e la verifica. In termini di test del linguaggio, questa funzionalità usa i termini seguenti:
- 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.
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
});
});
Quando si usano simulazioni nei test, il codice del modello deve effettuare la simulazione per testare la funzione senza chiamare la dipendenza sottostante utilizzata nella funzione, come 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, ha una configurazione aggiuntiva.
Esistono diverse parti del file di test:
-
import: le istruzioni import consentono di usare o simulare uno dei test. -
mock: creare il comportamento fittizio predefinito desiderato. Ogni test può modificarsi in base alle esigenze. -
describe: gruppo di test per ilinsert.tsfile. -
it: Ogni test per il fileinsert.ts.
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 seguente illustra come testare la funzione insertDocument .
// 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,
});
});
});
Risoluzione dei problemi
La maggior parte del codice in questo articolo proviene dal repository GitHub MicrosoftDocs/node-essentials . Se si vuole inserire in una risorsa cloud di Cosmos DB, creare la risorsa con questo script.