Testen der Azure SDK-Integration in JavaScript-Anwendungen
Das Testen Ihres Integrationscodes für das Azure SDK für JavaScript ist unerlässlich, um sicherzustellen, dass Ihre Anwendungen ordnungsgemäß mit Azure-Diensten interagieren.
Bei der Entscheidung, ob Clouddienst SDK-Aufrufe simuliert oder ein Livedienst zu Testzwecken verwendet werden soll, ist es wichtig, die Kompromisse zwischen Geschwindigkeit, Zuverlässigkeit und Kosten zu berücksichtigen.
Voraussetzungen
Simulierte Clouddienste
Vorteile:
- Beschleunigt die Testsuite durch Eliminierung der Netzwerklatenz.
- Stellt vorhersehbare und kontrollierte Testumgebungen bereit.
- Einfacheres Mocking verschiedener Szenarien und Randfälle.
- Reduziert die Kosten für die Verwendung von Live-Clouddiensten, insbesondere in kontinuierlichen Integrationspipelines.
Nachteile:
- Pseudodaten können vom tatsächlichen SDK abweichen, was zu Diskrepanzen führt.
- Möglicherweise werden bestimmte Features oder Verhaltensweisen des Livediensts ignoriert.
- Weniger realistische Umgebung im Vergleich zur Produktion.
Verwenden eines Livediensts
Vorteile:
- Bietet eine realistische Umgebung, die die Produktion genau widerspiegelt.
- Nützlich für Integrationstests, um sicherzustellen, dass verschiedene Teile des Systems zusammenarbeiten.
- Hilft bei der Identifizierung von Problemen im Zusammenhang mit der Netzwerkzuverlässigkeit, der Dienstverfügbarkeit und der tatsächlichen Datenverarbeitung.
Nachteile:
- Langsamer aufgrund von Netzwerkanrufen.
- Teurer aufgrund möglicher Kosten für die Nutzung von Diensten.
- Komplex und zeitaufwendig, eine Live-Service-Umgebung einzurichten und zu warten, die der Produktion entspricht.
Die Wahl zwischen Mocking und Nutzung von Livediensten hängt von Ihrer Teststrategie ab. Bei Komponententests, bei denen Geschwindigkeit und Kontrolle von größter Bedeutung sind, ist Mocking oft die bessere Wahl. Bei Integrationstests, bei denen Realismus entscheidend ist, kann die Verwendung eines Livedienstes genauere Ergebnisse liefern. Das Ausgleichen dieser Ansätze hilft dabei, eine umfassende Testabdeckung zu erzielen und gleichzeitig Kosten zu verwalten und die Testeffizienz aufrechtzuerhalten.
Testdoubles: Pseudodaten, Stubs und Fälschungen
Ein Testdouble ist jede Art von Ersatz, der anstelle von etwas Realem zu Testzwecken verwendet wird. Die Art des Doubles, das Sie auswählen, hängt davon ab, was Sie ersetzen möchten. Der Begriff Simulation meint häufig einen Double, wenn der Begriff beiläufig verwendet wird. In diesem Artikel wird der Begriff speziell und im Jest-Testframework veranschaulicht.
Mocks
Mocks (auch als Spies bezeichnet): Können eine Funktion ersetzen und das Verhalten dieser Funktion steuern und ausspähen, wenn sie indirekt von einem anderen Code aufgerufen wird.
In den folgenden Beispielen geht es um zwei Funktionen:
- someTestFunction: Dies ist die Funktion, die Sie testen müssen. Sie ruft die Abhängigkeit
dependencyFunction
auf, die Sie nicht geschrieben haben und nicht testen müssen. - dependencyFunctionMock: Dies ist eine Mocking der Abhängigkeit.
// setup
const dependencyFunctionMock = jest.fn();
// perform test
// Jest replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// verify behavior
expect(dependencyFunctionMock).toHaveBeenCalled();
Der Test dient dazu, sicherzustellen, dass sich someTestFunction ordnungsgemäß verhält, ohne tatsächlich den Abhängigkeitscode aufrufen zu müssen. Der Test überprüft, ob der Mock der Abhängigkeit aufgerufen wurde.
Mocking großer und kleiner Abhängigkeiten
Wenn Sie sich entscheiden, eine Abhängigkeit zu simulieren, können Sie auswählen, was Sie benötigen. Beispiel:
- Eine Funktion oder zwei aus einer größeren Abhängigkeit. Jest bietet zu diesem Zweck TeilMocks.
- Alle Funktionen einer kleineren Abhängigkeit, wie im Beispiel in diesem Artikel gezeigt.
Stubs
Der Zweck von Stubs besteht darin, die Rückgabedaten einer Funktion zu ersetzen, um verschiedene Szenarien zu simulieren. Auf diese Weise kann Ihr Code die Funktion aufrufen und verschiedene Zustände empfangen, einschließlich erfolgreicher Ergebnisse, Fehler, Ausnahmen und Randfälle. Die Zustandsüberprüfung stellt sicher, dass Ihr Code diese Szenarien ordnungsgemäß verarbeitet.
// 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}`);
Der Zweck des vorherigen Tests besteht darin, sicherzustellen, dass durch die von someTestFunction
ausgeführte Aufgabe das erwartete Ergebnis erreicht wird. In diesem einfachen Beispiel besteht die Aufgabe der Funktion darin, die Vor- und Familiennamen zu verketten. Durch die Verwendung von Fakedaten kennen Sie das erwartete Ergebnis und können überprüfen, ob die Funktion die Aufgabe korrekt ausführt.
Fakes
Fakes ersetzen eine Funktionalität, die Sie normalerweise nicht in der Produktion verwenden würden, wie z. B. die Verwendung einer In-Memory-Datenbank anstelle einer Clouddatenbank.
// 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);
});
});
Der Zweck des vorherigen Tests besteht darin, sicherzustellen, dass someTestFunction
ordnungsgemäß mit der Datenbank interagiert. Mithilfe einer In-Memory-Fakedatenbank können Sie die Logik der Funktion testen, eine echte Datenbank angewiesen zu sein und die Tests schneller und zuverlässiger zu machen.
Szenario: Einfügen eines Dokuments in Cosmos DB mithilfe des Azure SDK
Angenommen, Sie haben eine Anwendung, die ein neues Dokument in Cosmos DB schreiben muss, wenn alle Informationen übermittelt und überprüft werden. Wenn ein leeres Formular übermittelt wird oder die Informationen nicht mit dem erwarteten Format übereinstimmen, sollte die Anwendung die Daten nicht eingeben.
Cosmos DB wird als Beispiel verwendet, die Konzepte gelten jedoch für die meisten Azure SDKs für JavaScript. Die folgende Funktion erfasst diese Funktionalität:
// 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;
}
}
}
Hinweis
TypeScript-Typen helfen beim Definieren der Arten von Daten, die eine Funktion verwendet. Obwohl Sie TypeScript nicht benötigen, um Jest oder andere JavaScript-Testframeworks zu verwenden, ist es für das Schreiben von typsicherem JavaScript unerlässlich.
Die Funktionen in der oben genannten Anwendung sind:
Funktion | Beschreibung |
---|---|
insertDocument | Fügt ein Dokument in die Datenbank ein. Das soll getestet werden. |
inputVerified | Überprüft die Eingabedaten anhand eines Schemas. Stellt sicher, dass Daten im richtigen Format vorliegen (z. B. gültige E-Mail-Adressen, korrekt formatierte URLs). |
cosmos.items.create | SDK-Funktion für Azure Cosmos DB mithilfe von @azure/cosmos. Das soll simuliert werden. Verfügt bereits über eigene Tests, die von den Paketbesitzern verwaltet werden. Wir müssen überprüfen, ob der Cosmos DB-Funktionsaufruf erfolgt ist und Daten zurückgegeben wurden, wenn die eingehenden Daten die Überprüfung bestanden haben. |
Installieren der Testframeworkabhängigkeit
In diesem Artikel wird Jest als Testframework verwendet. Sie können auch andere, vergleichbare Testframeworks verwenden.
Installieren Sie Jest im Stammverzeichnis des Anwendungsverzeichnisses mit dem folgenden Befehl:
npm install jest
Konfigurieren des Pakets zum Ausführen des Tests
Aktualisieren Sie package.json
für die Anwendung mit einem neuen Skript, um unsere Quellcodedateien zu testen. Quellcode-Dateien werden durch den Abgleich von Dateinamen und Erweiterung definiert. Jest sucht nach Dateien gemäß der allgemeinen Benennungskonvention für Testdateien: <file-name>.spec.[jt]s
. Dieses Muster bedeutet, dass Dateien, die wie die folgenden Beispiele benannt sind, als Testdateien interpretiert und von Jest ausgeführt werden:
- *.test.js: Beispiel: math.test.js
- *.spec.js: Beispiel: math.spec.js
- Dateien, die sich im Verzeichnis tests befinden, wie tests/math.js
Fügen Sie dem package.json ein Skript hinzu, um dieses Testdateimuster mit Jest zu unterstützen:
"scripts": {
"test": "jest dist",
}
Der TypeScript-Quellcode wird im dist
-Unterordner generiert. Jest führt die .spec.js
im Unterordner gefundenen Dateien dist
aus.
Einrichten des Komponententests für das Azure SDK
Wie können wir Mocks, Stubs und Fakes verwenden, um die insertDocument-Funktion zu testen?
- Mocks: Wir benötigen eine Simulation, um sicherzustellen, dass das Verhalten der Funktion getestet wird. Beispiel:
- Wenn die Daten die Überprüfung bestehen, ist der Aufruf der Cosmos DB-Funktion nur einmal erfolgt.
- Wenn die Daten die Überprüfung nicht bestehen, ist kein Aufruf der Cosmos DB-Funktion erfolgt.
- Stubs:
- Die übergebenen Daten entsprechen dem neuen Dokument, das von der Funktion zurückgegeben wird.
Denken Sie beim Testen an den Testaufbau, den Test selbst und die Verifizierung. In der Fachsprache der Tests wird dies wie folgt bezeichnet:
- Anordnen: richten Sie die Testbedingungen ein
- Handeln: Rufen Sie die zu testende Funktion auf, auch bekannt als system under test oder SUT
- Bestätigen: überprüfen Sie die Ergebnisse. Ergebnisse können Verhalten oder Zustand sein.
- Das Verhalten gibt die Funktionalität in Ihrer Testfunktion an, die überprüft werden kann. Ein Beispiel dafür ist, dass einige Abhängigkeiten aufgerufen wurden.
- State gibt die von der Funktion zurückgegebenen Daten an.
Ähnlich wie andere Testframeworks verfügt Jest über Testdateibausteine, um Ihre Testdatei zu definieren.
// 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
});
});
Wenn Sie Mocks verwenden, muss dieser Boilerplate-Platz Mocking verwenden, um die Funktion zu testen, ohne die zugrunde liegende Abhängigkeit aufzurufen, die in der Funktion verwendet wird, wie z. B. die Azure-Clientbibliotheken.
Erstellen der Testdatei
Die Testdatei mit Mocks zur Simulation eines Aufrufs einer Abhängigkeit enthält neben dem allgemeinen Test-Boilerplate-Code einige zusätzliche Einstellungen. Es gibt mehrere Teile der folgenden Testdatei:
import
: Mit den Importanweisungen können Sie einen Ihrer Tests verwenden oder simulieren.jest.mock
: Erstellen Sie das gewünschte standardmäßige Scheinverhalten. Jeder Test kann bei Bedarf geändert werden.describe
: Testgruppenfamilie für dieinsert.ts
-Datei.test
: Jeder Test für dieinsert.ts
-Datei.
Die Testdatei umfasst drei Tests für die insert.ts
-Datei, die in zwei Überprüfungstypen unterteilt werden kann:
Überprüfungstyp | Testen |
---|---|
Positiver Pfad: should insert document successfully |
Die simulierte Datenbankmethode wurde aufgerufen und hat geänderte Daten zurückgegeben. |
Fehlerpfad: should return verification error if input is not verified |
Fehler bei der Überprüfung der Daten und Rückgabe eines Fehlers. |
Fehlerpfad: should return error if db insert fails |
Die simulierte Datenbankmethode wurde aufgerufen und hat einen Fehler zurückgegeben. |
Die folgende Jest-Testdatei zeigt, wie die insertDocument-Funktion getestet wird.
// 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,
});
});
});
Weitere Informationen
- Bewährte Methoden für Jest Mocking
- The Difference between Mocks and Stubs (Der Unterschied zwischen Mocks und Stubs) von Martin Fowler