Deutsch

Meistern Sie die testgetriebene Entwicklung (TDD) in JavaScript. Dieser umfassende Leitfaden behandelt den Red-Green-Refactor-Zyklus, die praktische Umsetzung mit Jest und Best Practices für die moderne Entwicklung.

Testgetriebene Entwicklung in JavaScript: Ein umfassender Leitfaden für globale Entwickler

Stellen Sie sich folgendes Szenario vor: Sie haben die Aufgabe, ein kritisches Stück Code in einem großen Altsystem zu ändern. Sie spüren ein Gefühl der Furcht. Wird Ihre Änderung etwas anderes kaputt machen? Wie können Sie sicher sein, dass das System immer noch wie vorgesehen funktioniert? Diese Angst vor Veränderungen ist ein häufiges Leiden in der Softwareentwicklung und führt oft zu langsamem Fortschritt und fragilen Anwendungen. Aber was wäre, wenn es eine Möglichkeit gäbe, Software mit Zuversicht zu entwickeln und ein Sicherheitsnetz zu schaffen, das Fehler abfängt, bevor sie jemals die Produktion erreichen? Das ist das Versprechen der testgetriebenen Entwicklung (TDD).

TDD ist nicht nur eine Testtechnik; es ist ein disziplinierter Ansatz für Softwaredesign und -entwicklung. Es kehrt das traditionelle Modell „Code schreiben, dann testen“ um. Mit TDD schreiben Sie einen Test, der fehlschlägt bevor Sie den Produktionscode schreiben, der ihn bestehen lässt. Diese einfache Umkehrung hat tiefgreifende Auswirkungen auf Codequalität, Design und Wartbarkeit. Dieser Leitfaden bietet einen umfassenden, praktischen Einblick in die Implementierung von TDD in JavaScript, konzipiert für ein globales Publikum von professionellen Entwicklern.

Was ist Testgetriebene Entwicklung (TDD)?

Im Kern ist die testgetriebene Entwicklung ein Entwicklungsprozess, der auf der Wiederholung eines sehr kurzen Entwicklungszyklus beruht. Anstatt Features zu schreiben und sie dann zu testen, besteht TDD darauf, dass der Test zuerst geschrieben wird. Dieser Test wird unweigerlich fehlschlagen, da das Feature noch nicht existiert. Die Aufgabe des Entwicklers ist es dann, den einfachstmöglichen Code zu schreiben, um diesen spezifischen Test zu bestehen. Sobald er besteht, wird der Code bereinigt und verbessert. Dieser grundlegende Kreislauf ist als „Red-Green-Refactor“-Zyklus bekannt.

Der Rhythmus von TDD: Red-Green-Refactor

Dieser dreistufige Zyklus ist der Herzschlag von TDD. Das Verstehen und Praktizieren dieses Rhythmus ist grundlegend, um die Technik zu meistern.

Sobald der Zyklus für ein kleines Stück Funktionalität abgeschlossen ist, beginnen Sie erneut mit einem neuen fehlschlagenden Test für das nächste Stück.

Die drei Gesetze von TDD

Robert C. Martin (oft als „Uncle Bob“ bekannt), eine Schlüsselfigur in der agilen Softwarebewegung, definierte drei einfache Regeln, die die TDD-Disziplin kodifizieren:

  1. Sie dürfen keinen Produktionscode schreiben, es sei denn, um einen fehlschlagenden Unit-Test zum Bestehen zu bringen.
  2. Sie dürfen nicht mehr von einem Unit-Test schreiben, als ausreicht, um ihn fehlschlagen zu lassen; und Kompilierungsfehler sind Fehlschläge.
  3. Sie dürfen nicht mehr Produktionscode schreiben, als ausreicht, um den einen fehlschlagenden Unit-Test zum Bestehen zu bringen.

Das Befolgen dieser Gesetze zwingt Sie in den Red-Green-Refactor-Zyklus und stellt sicher, dass 100 % Ihres Produktionscodes geschrieben werden, um eine spezifische, getestete Anforderung zu erfüllen.

Warum sollten Sie TDD einführen? Der globale Business Case

Während TDD einzelnen Entwicklern immense Vorteile bietet, wird seine wahre Stärke auf Team- und Geschäftsebene realisiert, insbesondere in global verteilten Umgebungen.

Einrichten Ihrer JavaScript-TDD-Umgebung

Um mit TDD in JavaScript zu beginnen, benötigen Sie einige Werkzeuge. Das moderne JavaScript-Ökosystem bietet hervorragende Auswahlmöglichkeiten.

Kernkomponenten eines Test-Stacks

Aufgrund seiner Einfachheit und seines All-in-One-Charakters werden wir Jest für unsere Beispiele verwenden. Es ist eine ausgezeichnete Wahl für Teams, die eine „Zero-Configuration“-Erfahrung suchen.

Schritt-für-Schritt-Einrichtung mit Jest

Lassen Sie uns ein neues Projekt für TDD einrichten.

1. Initialisieren Sie Ihr Projekt: Öffnen Sie Ihr Terminal und erstellen Sie ein neues Projektverzeichnis.

mkdir js-tdd-projekt
cd js-tdd-projekt
npm init -y

2. Installieren Sie Jest: Fügen Sie Jest als Entwicklungsabhängigkeit zu Ihrem Projekt hinzu.

npm install --save-dev jest

3. Konfigurieren Sie das Test-Skript: Öffnen Sie Ihre `package.json`-Datei. Suchen Sie den Abschnitt `"scripts"` und ändern Sie das `"test"`-Skript. Es wird auch dringend empfohlen, ein `"test:watch"`-Skript hinzuzufügen, das für den TDD-Workflow von unschätzbarem Wert ist.

"scripts": {
  "test": "jest",
  "test:watch": "jest --watchAll"
}

Das `--watchAll`-Flag weist Jest an, Tests automatisch erneut auszuführen, wenn eine Datei gespeichert wird. Dies liefert sofortiges Feedback, was perfekt für den Red-Green-Refactor-Zyklus ist.

Das war's! Ihre Umgebung ist bereit. Jest findet automatisch Testdateien, die `*.test.js`, `*.spec.js` heißen oder sich in einem `__tests__`-Verzeichnis befinden.

TDD in der Praxis: Erstellen eines `CurrencyConverter`-Moduls

Lassen Sie uns den TDD-Zyklus auf ein praktisches, global verständliches Problem anwenden: die Umrechnung von Geld zwischen Währungen. Wir werden schrittweise ein `CurrencyConverter`-Modul erstellen.

Iteration 1: Einfache, festkursbasierte Umrechnung

🔴 ROT: Schreiben Sie den ersten fehlschlagenden Test

Unsere erste Anforderung ist die Umrechnung eines bestimmten Betrags von einer Währung in eine andere zu einem festen Kurs. Erstellen Sie eine neue Datei namens `CurrencyConverter.test.js`.

// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');

describe('CurrencyConverter', () => {
  it('sollte einen Betrag korrekt von USD nach EUR umrechnen', () => {
    // Arrange
    const amount = 10; // 10 USD
    const expected = 9.2; // Annahme eines festen Kurses von 1 USD = 0.92 EUR

    // Act
    const result = CurrencyConverter.convert(amount, 'USD', 'EUR');

    // Assert
    expect(result).toBe(expected);
  });
});

Führen Sie nun den Test-Watcher von Ihrem Terminal aus:

npm run test:watch

Der Test wird spektakulär fehlschlagen. Jest wird etwas wie `TypeError: Cannot read properties of undefined (reading 'convert')` melden. Dies ist unser ROTER Zustand. Der Test schlägt fehl, weil `CurrencyConverter` nicht existiert.

🟢 GRÜN: Schreiben Sie den einfachsten Code, um zu bestehen

Lassen Sie uns nun den Test bestehen. Erstellen Sie `CurrencyConverter.js`.

// CurrencyConverter.js
const rates = {
  USD: {
    EUR: 0.92
  }
};

const CurrencyConverter = {
  convert(amount, from, to) {
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

Sobald Sie diese Datei speichern, führt Jest den Test erneut aus und er wird GRÜN. Wir haben den absolut minimalen Code geschrieben, um die Anforderung des Tests zu erfüllen.

🔵 REFACTOR: Verbessern Sie den Code

Der Code ist einfach, aber wir können bereits über Verbesserungen nachdenken. Das verschachtelte `rates`-Objekt ist etwas starr. Fürs Erste ist es sauber genug. Das Wichtigste ist, dass wir ein funktionierendes Feature haben, das durch einen Test geschützt ist. Gehen wir zur nächsten Anforderung über.

Iteration 2: Umgang mit unbekannten Währungen

🔴 ROT: Schreiben Sie einen Test für eine ungültige Währung

Was sollte passieren, wenn wir versuchen, in eine Währung umzurechnen, die wir nicht kennen? Es sollte wahrscheinlich einen Fehler auslösen. Definieren wir dieses Verhalten in einem neuen Test in `CurrencyConverter.test.js`.

// In CurrencyConverter.test.js, innerhalb des describe-Blocks

it('sollte bei unbekannten Währungen einen Fehler auslösen', () => {
  // Arrange
  const amount = 10;

  // Act & Assert
  // Wir umschließen den Funktionsaufruf in einer Pfeilfunktion, damit toThrow von Jest funktioniert.
  expect(() => {
    CurrencyConverter.convert(amount, 'USD', 'XYZ');
  }).toThrow('Unbekannte Währung: XYZ');
});

Speichern Sie die Datei. Der Test-Runner zeigt sofort einen neuen Fehlschlag an. Er ist ROT, weil unser Code keinen Fehler auslöst; er versucht, auf `rates['USD']['XYZ']` zuzugreifen, was zu einem `TypeError` führt. Unser neuer Test hat diesen Fehler korrekt identifiziert.

🟢 GRÜN: Lassen Sie den neuen Test bestehen

Ändern wir `CurrencyConverter.js`, um die Validierung hinzuzufügen.

// CurrencyConverter.js
const rates = {
  USD: {
    EUR: 0.92,
    GBP: 0.80
  },
  EUR: {
    USD: 1.08
  }
};

const CurrencyConverter = {
  convert(amount, from, to) {
    if (!rates[from] || !rates[from][to]) {
      // Bestimmen, welche Währung unbekannt ist, für eine bessere Fehlermeldung
      const unknownCurrency = !rates[from] ? from : to;
      throw new Error(`Unbekannte Währung: ${unknownCurrency}`);
    }
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

Speichern Sie die Datei. Beide Tests bestehen nun. Wir sind zurück bei GRÜN.

🔵 REFACTOR: Räumen Sie auf

Unsere `convert`-Funktion wächst. Die Validierungslogik ist mit der Berechnung vermischt. Wir könnten die Validierung in eine separate private Funktion extrahieren, um die Lesbarkeit zu verbessern, aber im Moment ist es noch überschaubar. Der Schlüssel ist, dass wir die Freiheit haben, diese Änderungen vorzunehmen, weil unsere Tests uns sagen werden, wenn wir etwas kaputt machen.

Iteration 3: Asynchrones Abrufen von Kursen

Das Hartcodieren von Kursen ist nicht realistisch. Lassen Sie uns unser Modul umgestalten, um Kurse von einer (gemockten) externen API abzurufen.

🔴 ROT: Schreiben Sie einen asynchronen Test, der einen API-Aufruf mockt

Zuerst müssen wir unseren Konverter umstrukturieren. Er muss jetzt eine Klasse sein, die wir instanziieren können, vielleicht mit einem API-Client. Wir müssen auch die `fetch`-API mocken. Jest macht das einfach.

Schreiben wir unsere Testdatei neu, um dieser neuen, asynchronen Realität gerecht zu werden. Wir beginnen damit, den Erfolgsfall erneut zu testen.

// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');

// Die externe Abhängigkeit mocken
global.fetch = jest.fn();

beforeEach(() => {
  // Mock-Verlauf vor jedem Test löschen
  fetch.mockClear();
});

describe('CurrencyConverter', () => {
  it('sollte Kurse abrufen und korrekt umrechnen', async () => {
    // Arrange
    // Die erfolgreiche API-Antwort mocken
    fetch.mockResolvedValueOnce({
      json: () => Promise.resolve({ rates: { EUR: 0.92 } })
    });

    const converter = new CurrencyConverter('https://api.exchangerates.com');
    const amount = 10; // 10 USD

    // Act
    const result = await converter.convert(amount, 'USD', 'EUR');

    // Assert
    expect(result).toBe(9.2);
    expect(fetch).toHaveBeenCalledTimes(1);
    expect(fetch).toHaveBeenCalledWith('https://api.exchangerates.com/latest?base=USD');
  });

  // Wir würden auch Tests für API-Fehler usw. hinzufügen.
});

Das Ausführen dieses Codes wird zu einem Meer von ROT führen. Unser alter `CurrencyConverter` ist keine Klasse, hat keine `async`-Methode und verwendet `fetch` nicht.

🟢 GRÜN: Implementieren Sie die asynchrone Logik

Schreiben wir nun `CurrencyConverter.js` neu, um die Anforderungen des Tests zu erfüllen.

// CurrencyConverter.js
class CurrencyConverter {
  constructor(apiUrl) {
    this.apiUrl = apiUrl;
  }

  async convert(amount, from, to) {
    const response = await fetch(`${this.apiUrl}/latest?base=${from}`);
    if (!response.ok) {
      throw new Error('Fehler beim Abrufen der Wechselkurse.');
    }

    const data = await response.json();
    const rate = data.rates[to];

    if (!rate) {
      throw new Error(`Unbekannte Währung: ${to}`);
    }

    // Einfaches Runden, um Gleitkommaprobleme in Tests zu vermeiden
    const convertedAmount = amount * rate;
    return parseFloat(convertedAmount.toFixed(2));
  }
}

module.exports = CurrencyConverter;

Wenn Sie speichern, sollte der Test GRÜN werden. Beachten Sie, dass wir auch eine Rundungslogik hinzugefügt haben, um Gleitkomma-Ungenauigkeiten zu behandeln, ein häufiges Problem bei Finanzberechnungen.

🔵 REFACTOR: Verbessern Sie den asynchronen Code

Die `convert`-Methode macht viel: Abrufen, Fehlerbehandlung, Parsen und Berechnen. Wir könnten dies refaktorisieren, indem wir eine separate `RateFetcher`-Klasse erstellen, die nur für die API-Kommunikation verantwortlich ist. Unser `CurrencyConverter` würde dann diesen Fetcher verwenden. Dies folgt dem Single-Responsibility-Prinzip und macht beide Klassen einfacher zu testen und zu warten. TDD leitet uns zu diesem saubereren Design.

Gängige TDD-Muster und Anti-Muster

Während Sie TDD praktizieren, werden Sie Muster entdecken, die gut funktionieren, und Anti-Muster, die Reibung verursachen.

Gute Muster zum Befolgen

Zu vermeidende Anti-Muster

TDD im breiteren Entwicklungslebenszyklus

TDD existiert nicht im luftleeren Raum. Es integriert sich wunderbar in moderne Agile- und DevOps-Praktiken, insbesondere für globale Teams.

Fazit: Ihre Reise mit TDD

Testgetriebene Entwicklung ist mehr als eine Teststrategie – es ist ein Paradigmenwechsel in der Herangehensweise an die Softwareentwicklung. Sie fördert eine Kultur der Qualität, des Vertrauens und der Zusammenarbeit. Der Red-Green-Refactor-Zyklus bietet einen stetigen Rhythmus, der Sie zu sauberem, robustem und wartbarem Code führt. Die resultierende Test-Suite wird zu einem Sicherheitsnetz, das Ihr Team vor Regressionen schützt, und zu einer lebenden Dokumentation, die neue Mitglieder einarbeitet.

Die Lernkurve kann steil erscheinen, und das anfängliche Tempo mag langsamer wirken. Aber die langfristigen Dividenden in Form von reduzierter Debugging-Zeit, verbessertem Softwaredesign und gesteigertem Entwicklervertrauen sind unermesslich. Der Weg zur Beherrschung von TDD ist einer von Disziplin und Übung.

Beginnen Sie noch heute. Wählen Sie ein kleines, unkritisches Feature in Ihrem nächsten Projekt und verpflichten Sie sich dem Prozess. Schreiben Sie den Test zuerst. Sehen Sie ihm beim Fehlschlagen zu. Bringen Sie ihn zum Bestehen. Und dann, am wichtigsten, refaktorisieren Sie. Erleben Sie das Vertrauen, das von einer grünen Test-Suite ausgeht, und Sie werden sich bald fragen, wie Sie jemals Software auf andere Weise entwickelt haben.

Testgetriebene Entwicklung (TDD) in JavaScript: Ein umfassender Leitfaden für globale Entwickler | MLOG