5.2 KI-gestützte Testautomatisierung
Arbeiten Sie an einem neuen Feature für Ihre Applikation, kommt irgendwann der Zeitpunkt, an dem Sie die Funktionalität überprüfen müssen. Eine manuelle Überprüfung ist zeitaufwendig und fehleranfällig, vor allem wenn Sie bedenken, dass Sie zumindest Teile der übrigen Applikation ebenfalls testen müssen, da es ja sein kann, dass sich durch die Integration an anderen Stellen Fehler eingeschlichen haben. Das Automatisierungspotenzial bei Aufgaben wie der Überprüfung von Features einer Applikation ist groß. Genau aus diesem Grund gibt es die Disziplin der Testautomatisierung. Sie können Ihre Applikation auf verschiedenen Ebenen testen. Diese werden in der sogenannten Testpyramide beschrieben. Sie ordnet die verschiedenen Arten von Tests auf unterschiedlichen Ebenen ein:
-
Unittests: Das Fundament der Pyramide bilden die Unittests. Sie überprüfen die kleinsten testbaren Einheiten einer Applikation in Isolation. Diese Einheiten sind meist Funktionen oder Methoden. Unittests werden schnell abgearbeitet, sind einfach zu schreiben und meist in großer Anzahl vorhanden.
-
Integrationstests: Diese Tests bilden die mittlere Ebene der Testpyramide. Sie überprüfen einzelne oder mehrere Module oder Komponenten. Sie sind komplexer und langsamer als Unittests, bieten jedoch auch mehr Sicherheit, da sie das Zusammenspiel mehrerer Bestandteile Ihrer Applikation testen. Typischerweise weist Ihre Applikation deutlich weniger Integrationstests als Unittests auf.
-
Ende-zu-Ende-Tests: Die Spitze der Pyramide bilden die Ende-zu-Ende-Tests oder kurz E2E-Tests. Sie überprüfen Features über die gesamte Applikation vom Frontend bis zur Datenhaltung. Sie sind am aufwendigsten zu erstellen und laufen am langsamsten, bieten dafür jedoch das höchste Maß an Sicherheit. Im Vergleich zu den anderen beiden Ebenen weist Ihre Applikation typischerweise von den E2E-Tests am wenigsten auf.
KI-Werkzeuge können Sie auf allen Ebenen der Testpyramide unterstützen und Ihnen einige Arbeit bei der Erzeugung von Tests abnehmen. Im weiteren Verlauf dieses Kapitels lernen Sie die verschiedenen Ebenen der Testpyramide und ihre Eigenheiten am Beispiel von TypeScript kennen. Tests funktionieren in anderen Programmiersprachen und Umgebungen jedoch ähnlich, sodass Sie Ihr Wissen übertragen können.
KI gestützte Unittests erstellen
Für Unittests in JavaScript und TypeScript gibt es einige Testframeworks. Eines der populärsten ist Vitest, ein Testframework, das aus dem Vue.js-Ökosystem stammt, aber framework- und sogar plattformunabhängig ist. Sie können das Testframework also sowohl clientseitig im Browser als auch serverseitig mit Node.js nutzen. Ein großer Vorteil von Vitest ist, dass es in einfachen Fällen ganz ohne Konfiguration auskommt. Es findet die Tests automatisch anhand ihres Dateinamens und führt sie aus.
KI gestützte Unittests
Der erste Schritt bei der KI-gestützten Erzeugung eines Unittests ist eine einfache TypeScript-Funktion mit dem Namen add, die zwei Ganzzahlen addiert und das Ergebnis zurückgibt. Der Code dieser Funktion sieht folgendermaßen aus:
export default function add(a: number, b: number): number {
if (!Number.isSafeInteger(a) || !Number.isSafeInteger(b)) {
throw new Error("Can only add integers");
}
return a + b;
}
Für diese Funktion können Sie sich von ChatGPT einen Test generieren lassen:
Prompt: Please write a vitest unit test for the following function:
(Code der Funktion …)
Create only the test, the environment is already set up and everything is installed.
import { describe, it, expect } from 'vitest';
import add from './add'; // Adjust path to function add
describe('add', () => {
it('should add two integers correctly', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
it(
'should throw an error if either argument is not a safe ' +
'integer', () => {
expect(() => add(2.5, 3))
.toThrow('Can only add integers');
expect(() => add(2, 3.5))
.toThrow('Can only add integers');
expect(() => add(Number.MAX_SAFE_INTEGER + 1, 3))
.toThrow('Can only add integers');
expect(() => add(2, Number.MAX_SAFE_INTEGER + 1))
.toThrow('Can only add integers');
});
it(
'should throw an error if any argument is not a number',
() => {
expect(() => add('2', 3))
.toThrow('Can only add integers');
expect(() => add(2, '3'))
.toThrow('Can only add integers');
expect(() => add(null, 3))
.toThrow('Can only add integers');
expect(() => add(2, undefined))
.toThrow('Can only add integers');
});
});
… explanation of the scenarios …
Geben Sie beim Prompt, wie in diesem Beispiel, den gesamten Quellcode an, kann die KI alle Aspekte der Funktionalität abdecken. Einen Teil trägt die Signatur der Funktion bei, da Sie hier genau angeben, welche Datentypen Sie erwarten und welchen Typ Sie zurückgeben. Die Fehlerbehandlung innerhalb der Funktion weist auf zusätzliche Grenz- und Ausnahmefälle hin, die die Tests ebenfalls abdecken müssen. KI-Werkzeuge wie ChatGPT sind bei solchen Aufgaben gerne etwas ausführlicher in ihren Antworten und erzeugen nicht nur den gewünschten Test, sondern geben auch noch Tipps zum Setup und zur Ausführung. Das können Sie unterbinden, indem Sie angeben, dass Sie nur am Test und nicht an der Umgebung oder der Installation interessiert sind.
Das Ergebnis, das ChatGPT produziert hat, kann sich sehen lassen. Der generierte Code enthält nicht nur einen einfachen Test, der beispielsweise die Werte 2 und 3 addiert und prüft, ob das korrekte Ergebnis zurückgegeben wird, sondern eine ganze Reihe von Überprüfungen. Was hier positiv auffällt, ist, dass die Tests mit describe gruppiert sind und die Gruppe als Label den Namen der Funktion erhalten hat. Innerhalb dieser Gruppe gibt es drei Tests:
-
Erfolgsfall: Der Test erwartet, dass die Funktion in der Lage ist, zwei Ganzzahlen zu addieren. Die erste Überprüfung stellt sicher, dass zwei positive Ganzzahlen addiert werden können, die zweite nimmt noch eine negative Zahl hinzu, und die dritte Überprüfung stellt sicher, dass auch 0 und 0 addiert werden können.
-
Fehlerfall 1: Die vorgegebene Implementierung der add-Funktion überprüft, dass es sich bei beiden Zahlen um sogenannte Safe Integers handelt, also Ganzzahlen, die in einem Bereich liegen, in dem TypeScript beziehungsweise die JavaScript-Engine korrekt rechnen kann. Der zweite Test überprüft, dass das Rechnen mit Fließkommazahlen und zu großen Zahlen (Number.MAX_SAFE_INTEGER + 1) nicht möglich ist und zu einer Exception führt.
-
Fehlerfall 2: Noch weiter geht der dritte Test und stellt sicher, dass der Aufruf der add-Funktion mit ungültigen Datentypen wie Strings, null oder undefined zu einem Fehler führt.
Beim Erfolgsfall nutzt ChatGPT den toBe-Matcher, um zu überprüfen, ob das Ergebnis mit dem erwarteten Wert übereinstimmt. Die Wahl dieser Überprüfungsfunktion passt gut, da sie intern auf strikte Gleichheit prüft und keine Typkonvertierung zulässt. Schwierigkeiten macht diese Funktion nur, wenn Referenztypen wie Objekte im Spiel sind, was hier aber nicht der Fall ist. Die Fehlerfälle decken die Tests korrekt durch den toThrow-Matcher ab, mit dem Sie überprüfen können, ob eine Funktion einen Fehler wirft.
Kommen wir zu den Schwächen der generierten Tests. Ein großes Problem ist, dass der TypeScript-Compiler beim Verarbeiten des Codes einige Fehler bei den Tests meldet. Der dritte Test, der die Funktion mit unterschiedlichen Typen aufruft, ist so in TypeScript nicht möglich, da die Typen beim Aufruf nicht kompatibel mit der Signatur der Funktion sind. Der Prompt erwähnt zwar nichts von TypeScript und typsicherem Code, bei der Analyse der Funktion hätte das jedoch auffallen müssen.
Dieses Problem können Sie adressieren, indem Sie von ChatGPT verlangen, dass es validen TypeScript-Code produzieren soll. Dazu können Sie entweder den ursprünglichen Prompt anpassen oder die Anforderung wie hier zusätzlich angeben:
Prompt: Make sure the tests are written in valid TypeScript. Keep the test if any argument is not a number but fix it.
Sure, here is the corrected test suite in valid TypeScript:
(existing code ...)
it(
'should throw an error if any argument is not a number',
() => {
expect(() => add('2' as any, 3))
.toThrow('Can only add integers');
expect(() => add(2, '3' as any))
.toThrow('Can only add integers');
expect(() => add(null as any, 3))
.toThrow('Can only add integers');
expect(() => add(2, undefined as any))
.toThrow('Can only add integers');
});
});
In this version: Each invalid argument is explicitly cast to any to ensure the test case runs as expected while maintaining TypeScript type checks for valid arguments.
Im Prompt geben Sie sicherheitshalber an, dass der dritte Test auf jeden Fall beibehalten werden soll. Dieser ist in einer reinen TypeScript-Umgebung unnötig, da Sie die Funktion nicht mit einem falschen Typ ausführen können. Da TypeScript aber nur zur Compile-Zeit und nicht zur Laufzeit zum Einsatz kommt, kann es je nach Umgebung trotzdem dazu kommen, dass die falschen Datentypen verwendet werden. Die ursprünglichen beiden Tests bleiben unverändert, da sie bereits in gültigem TypeScript verfasst waren. Um die Fehler im dritten Test zu umgehen, nutzt ChatGPT hier die Typumwandlung von TypeScript und macht aus einem ungültigen String-Datentyp eine Zahl. Diese Anpassung ist zwar nicht elegant, stellt den TypeScript-Compiler jedoch zufrieden.
Ein weiterer Kritikpunkt ist, dass die Tests nicht nur eine Assertion, sondern immer mehrere aufweisen. Die Überprüfungen passen zwar immer thematisch zusammen, oft gibt es jedoch den Anspruch, dass jeder Test nur über eine Assertion verfügen soll. Der Grund hierfür ist, dass ein Test nur aus einem Grund fehlschlagen soll, um die Fehlersuche zu erleichtern. In diesem Fall liegt es bei Ihnen zu entscheiden, ob Sie lieber weniger und thematisch gruppierte Tests oder viele und feingranulare Tests haben möchten. Auch hier können Sie mit einer Erweiterung des Prompts Abhilfe schaffen, indem Sie ChatGPT darum bitten, für jede Expectation einen eigenen Test zu verfassen:
Prompt: Please put every expectation in its own test. And keep the grouping by putting the tests in their own sub-describe blocks.
Sure, here is the revised test suite with each expectation in its own test and grouped using sub-describe blocks:
import { describe, it, expect } from 'vitest';
import add from './add'; // Adjust path to function add
describe('add', () => {
describe('valid inputs', () => {
it('should add 2 and 3 correctly', () => {
expect(add(2, 3)).toBe(5);
});
... more tests ...
});
describe('invalid safe integer inputs', () => {
... more tests ...
});
describe('invalid non-number inputs', () => {
... more tests...
});
});
Explanation of the code …
Prompten Sie lediglich den ersten Satz, macht ChatGPT aus den ursprünglichen drei Tests insgesamt 11 Tests. Der zweite Satz des Prompts sorgt dafür, dass die Gruppierung nicht verloren geht und Sie den Überblick nicht so schnell verlieren.
Generell gilt, dass Sie sich mit dem Thema Testing auseinandersetzen müssen, obwohl Ihnen Werkzeuge wie ChatGPT, Copilot und Co. sehr viel Arbeit abnehmen können. Sie sollten sich mit dem Aufbau und der Organisation von Tests vertraut machen und ein grundlegendes Wissen über Testmuster und die Architektur von Tests entwickeln. Je präziser Sie Ihre Prompts formulieren, desto eher entspricht auch das Ergebnis Ihren Erwartungen, und der Code der Tests bleibt lesbar und langfristig wartbar.
Die Möglichkeiten der KI enden jedoch nicht bei der Formulierung von Tests; die Werkzeuge können Sie auch bei der testgetriebenen Entwicklung unterstützen.