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 ilinsert.ts
file.test
: ogni test per ilinsert.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,
});
});
});