Polski

Opanuj Test-Driven Development (TDD) w JavaScript. Przewodnik omawia cykl Red-Green-Refactor, implementację z Jest i najlepsze praktyki dla nowoczesnych deweloperów.

Test-Driven Development w JavaScript: Kompleksowy przewodnik dla globalnych deweloperów

Wyobraź sobie taki scenariusz: masz za zadanie zmodyfikować krytyczny fragment kodu w dużym, przestarzałym systemie. Czujesz strach. Czy twoja zmiana zepsuje coś innego? Jak możesz być pewien, że system nadal działa zgodnie z przeznaczeniem? Ten lęk przed zmianą jest częstą dolegliwością w tworzeniu oprogramowania, często prowadzącą do powolnego postępu i kruchych aplikacji. A co, jeśli istniałby sposób na budowanie oprogramowania z pewnością siebie, tworząc siatkę bezpieczeństwa, która wyłapuje błędy, zanim dotrą do produkcji? To jest obietnica Test-Driven Development (TDD).

TDD to nie tylko technika testowania; to zdyscyplinowane podejście do projektowania i tworzenia oprogramowania. Odwraca tradycyjny model „napisz kod, potem testuj”. W TDD piszesz test, który kończy się niepowodzeniem zanim napiszesz kod produkcyjny, który sprawi, że przejdzie. To proste odwrócenie ma głębokie implikacje dla jakości kodu, projektu i łatwości utrzymania. Ten przewodnik zapewni kompleksowe, praktyczne spojrzenie na wdrażanie TDD w JavaScript, przeznaczone dla globalnej publiczności profesjonalnych deweloperów.

Czym jest Test-Driven Development (TDD)?

W swej istocie Test-Driven Development to proces deweloperski, który opiera się na powtarzaniu bardzo krótkiego cyklu rozwojowego. Zamiast pisać funkcje, a następnie je testować, TDD nalega, aby test został napisany jako pierwszy. Ten test nieuchronnie zakończy się niepowodzeniem, ponieważ funkcja jeszcze nie istnieje. Zadaniem dewelopera jest napisanie najprostszego możliwego kodu, aby ten konkretny test przeszedł. Gdy test przejdzie, kod jest czyszczony i ulepszany. Ta fundamentalna pętla jest znana jako cykl „Red-Green-Refactor”.

Rytm TDD: Red-Green-Refactor

Ten trzyetapowy cykl jest sercem TDD. Zrozumienie i praktykowanie tego rytmu jest fundamentalne dla opanowania tej techniki.

Gdy cykl dla jednego małego fragmentu funkcjonalności jest zakończony, zaczynasz od nowa, z nowym, nieprzechodzącym testem dla kolejnego fragmentu.

Trzy Prawa TDD

Robert C. Martin (często znany jako „Wujek Bob”), kluczowa postać w ruchu Agile, zdefiniował trzy proste zasady, które kodyfikują dyscyplinę TDD:

  1. Nie możesz pisać kodu produkcyjnego, chyba że ma on na celu sprawienie, by nieprzechodzący test jednostkowy przeszedł.
  2. Nie możesz pisać więcej kodu testu jednostkowego, niż jest to wystarczające do jego niepowodzenia; a błędy kompilacji są niepowodzeniami.
  3. Nie możesz pisać więcej kodu produkcyjnego, niż jest to wystarczające do zaliczenia jednego nieprzechodzącego testu jednostkowego.

Przestrzeganie tych praw zmusza cię do wejścia w cykl Red-Green-Refactor i zapewnia, że 100% twojego kodu produkcyjnego jest napisane w celu zaspokojenia konkretnego, przetestowanego wymagania.

Dlaczego warto wdrożyć TDD? Globalne uzasadnienie biznesowe

Chociaż TDD oferuje ogromne korzyści poszczególnym programistom, jego prawdziwa moc realizuje się na poziomie zespołu i biznesu, zwłaszcza w globalnie rozproszonych środowiskach.

Konfiguracja środowiska TDD w JavaScript

Aby zacząć z TDD w JavaScript, potrzebujesz kilku narzędzi. Nowoczesny ekosystem JavaScript oferuje doskonałe wybory.

Podstawowe komponenty stosu testowego

Ze względu na swoją prostotę i kompleksowość, w naszych przykładach użyjemy Jest. To doskonały wybór dla zespołów poszukujących doświadczenia „zero-configuration”.

Konfiguracja krok po kroku z Jest

Skonfigurujmy nowy projekt pod TDD.

1. Zainicjuj swój projekt: Otwórz terminal i utwórz nowy katalog projektu.

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

2. Zainstaluj Jest: Dodaj Jest do swojego projektu jako zależność deweloperską.

npm install --save-dev jest

3. Skonfiguruj skrypt testowy: Otwórz plik `package.json`. Znajdź sekcję `"scripts"` i zmodyfikuj skrypt `"test"`. Bardzo zalecane jest również dodanie skryptu `"test:watch"`, który jest nieoceniony w przepływie pracy TDD.

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

Flaga `--watchAll` informuje Jest, aby automatycznie ponownie uruchamiał testy za każdym razem, gdy plik zostanie zapisany. Zapewnia to natychmiastową informację zwrotną, co jest idealne dla cyklu Red-Green-Refactor.

To wszystko! Twoje środowisko jest gotowe. Jest automatycznie znajdzie pliki testowe o nazwach `*.test.js`, `*.spec.js` lub znajdujące się w katalogu `__tests__`.

TDD w praktyce: Budowanie modułu `CurrencyConverter`

Zastosujmy cykl TDD do praktycznego, globalnie zrozumiałego problemu: przeliczania pieniędzy między walutami. Zbudujemy moduł `CurrencyConverter` krok po kroku.

Iteracja 1: Prosta konwersja po stałym kursie

🔴 CZERWONY: Napisz pierwszy nieprzechodzący test

Naszym pierwszym wymaganiem jest przeliczenie określonej kwoty z jednej waluty na drugą przy użyciu stałego kursu. Utwórz nowy plik o nazwie `CurrencyConverter.test.js`.

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

describe('CurrencyConverter', () => {
  it('should convert an amount from USD to EUR correctly', () => {
    // Arrange
    const amount = 10; // 10 USD
    const expected = 9.2; // Assuming a fixed rate of 1 USD = 0.92 EUR

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

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

Teraz uruchom obserwatora testów (test watcher) z terminala:

npm run test:watch

Test spektakularnie się nie powiedzie. Jest zgłosi coś w stylu `TypeError: Cannot read properties of undefined (reading 'convert')`. To jest nasz stan CZERWONY. Test kończy się niepowodzeniem, ponieważ `CurrencyConverter` nie istnieje.

🟢 ZIELONY: Napisz najprostszy kod, aby test przeszedł

Teraz sprawmy, aby test przeszedł. Utwórz plik `CurrencyConverter.js`.

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

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

module.exports = CurrencyConverter;

Gdy tylko zapiszesz ten plik, Jest ponownie uruchomi test, który zmieni kolor na ZIELONY. Napisaliśmy absolutne minimum kodu, aby spełnić wymagania testu.

🔵 REFAKTORYZACJA: Ulepsz kod

Kod jest prosty, ale już możemy pomyśleć o ulepszeniach. Zagnieżdżony obiekt `rates` jest trochę sztywny. Na razie jest wystarczająco czysty. Najważniejsze jest to, że mamy działającą funkcję chronioną przez test. Przejdźmy do następnego wymagania.

Iteracja 2: Obsługa nieznanych walut

🔴 CZERWONY: Napisz test dla nieprawidłowej waluty

Co powinno się stać, jeśli spróbujemy przeliczyć na walutę, której nie znamy? Prawdopodobnie powinien zostać rzucony błąd. Zdefiniujmy to zachowanie w nowym teście w pliku `CurrencyConverter.test.js`.

// In CurrencyConverter.test.js, inside the describe block

it('should throw an error for unknown currencies', () => {
  // Arrange
  const amount = 10;

  // Act & Assert
  // Opakowujemy wywołanie funkcji w funkcję strzałkową, aby metoda toThrow z Jest zadziałała.
  expect(() => {
    CurrencyConverter.convert(amount, 'USD', 'XYZ');
  }).toThrow('Unknown currency: XYZ');
});

Zapisz plik. Narzędzie do uruchamiania testów natychmiast pokaże nową porażkę. Jest CZERWONY, ponieważ nasz kod nie rzuca błędu; próbuje uzyskać dostęp do `rates['USD']['XYZ']`, co skutkuje błędem `TypeError`. Nasz nowy test poprawnie zidentyfikował tę wadę.

🟢 ZIELONY: Spraw, by nowy test przeszedł

Zmodyfikujmy plik `CurrencyConverter.js`, aby dodać walidację.

// 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]) {
      // Ustal, która waluta jest nieznana, aby uzyskać lepszy komunikat o błędzie
      const unknownCurrency = !rates[from] ? from : to;
      throw new Error(`Unknown currency: ${unknownCurrency}`);
    }
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

Zapisz plik. Oba testy teraz przechodzą. Wracamy do stanu ZIELONEGO.

🔵 REFAKTORYZACJA: Posprzątaj kod

Nasza funkcja `convert` rośnie. Logika walidacji jest wymieszana z obliczeniami. Moglibyśmy wydzielić walidację do osobnej prywatnej funkcji, aby poprawić czytelność, ale na razie jest to jeszcze do opanowania. Kluczowe jest to, że mamy swobodę wprowadzania tych zmian, ponieważ nasze testy poinformują nas, jeśli coś zepsujemy.

Iteracja 3: Asynchroniczne pobieranie kursów

Hardkodowanie kursów nie jest realistyczne. Zrefaktoryzujmy nasz moduł, aby pobierał kursy z (zamockowanego) zewnętrznego API.

🔴 CZERWONY: Napisz test asynchroniczny, który mockuje wywołanie API

Najpierw musimy zrestrukturyzować nasz konwerter. Teraz będzie musiał być klasą, którą możemy instancjonować, być może z klientem API. Będziemy również musieli zamockować API `fetch`. Jest to ułatwia.

Przepiszmy nasz plik testowy, aby dostosować go do tej nowej, asynchronicznej rzeczywistości. Zaczniemy od ponownego przetestowania ścieżki pomyślnej.

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

// Mockowanie zewnętrznej zależności
global.fetch = jest.fn();

beforeEach(() => {
  // Wyczyść historię mocka przed każdym testem
  fetch.mockClear();
});

describe('CurrencyConverter', () => {
  it('should fetch rates and convert correctly', async () => {
    // Arrange
    // Mockowanie pomyślnej odpowiedzi API
    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');
  });

  // Dodalibyśmy również testy na wypadek awarii API itp.
});

Uruchomienie tego spowoduje morze CZERWIENI. Nasz stary `CurrencyConverter` nie jest klasą, nie ma metody `async` i nie używa `fetch`.

🟢 ZIELONY: Zaimplementuj logikę asynchroniczną

Teraz przepiszmy plik `CurrencyConverter.js`, aby spełniał wymagania testu.

// 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('Failed to fetch exchange rates.');
    }

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

    if (!rate) {
      throw new Error(`Unknown currency: ${to}`);
    }

    // Proste zaokrąglenie, aby uniknąć problemów z liczbami zmiennoprzecinkowymi w testach
    const convertedAmount = amount * rate;
    return parseFloat(convertedAmount.toFixed(2));
  }
}

module.exports = CurrencyConverter;

Po zapisaniu, test powinien zmienić kolor na ZIELONY. Zauważ, że dodaliśmy również logikę zaokrąglania, aby obsłużyć niedokładności liczb zmiennoprzecinkowych, co jest częstym problemem w obliczeniach finansowych.

🔵 REFAKTORYZACJA: Ulepsz kod asynchroniczny

Metoda `convert` robi wiele: pobiera dane, obsługuje błędy, parsuje i oblicza. Moglibyśmy to zrefaktoryzować, tworząc osobną klasę `RateFetcher` odpowiedzialną tylko za komunikację z API. Nasz `CurrencyConverter` używałby wtedy tego fetchera. Jest to zgodne z Zasadą Pojedynczej Odpowiedzialności (Single Responsibility Principle) i sprawia, że obie klasy są łatwiejsze do testowania i utrzymania. TDD prowadzi nas w kierunku tego czystszego projektu.

Popularne wzorce i antywzorce w TDD

Praktykując TDD, odkryjesz wzorce, które działają dobrze i antywzorce, które powodują problemy.

Dobre wzorce do naśladowania

Antywzorce, których należy unikać

TDD w szerszym cyklu życia oprogramowania

TDD nie istnieje w próżni. Pięknie integruje się z nowoczesnymi praktykami Agile i DevOps, zwłaszcza w zespołach globalnych.

Podsumowanie: Twoja podróż z TDD

Test-Driven Development to więcej niż strategia testowania — to zmiana paradygmatu w naszym podejściu do tworzenia oprogramowania. Wspiera kulturę jakości, pewności siebie i współpracy. Cykl Red-Green-Refactor zapewnia stały rytm, który prowadzi do czystego, solidnego i łatwego w utrzymaniu kodu. Wynikowy zestaw testów staje się siatką bezpieczeństwa, która chroni zespół przed regresjami, oraz żywą dokumentacją, która ułatwia wdrażanie nowych członków.

Krzywa uczenia się może wydawać się stroma, a początkowe tempo może wydawać się wolniejsze. Ale długoterminowe korzyści w postaci skróconego czasu debugowania, lepszego projektu oprogramowania i zwiększonej pewności siebie deweloperów są nie do przecenienia. Droga do opanowania TDD to droga dyscypliny i praktyki.

Zacznij już dziś. Wybierz jedną małą, niekrytyczną funkcję w swoim następnym projekcie i zaangażuj się w proces. Najpierw napisz test. Zobacz, jak zawodzi. Spraw, by przeszedł. A potem, co najważniejsze, zrefaktoryzuj. Doświadcz pewności, jaka płynie z zielonego zestawu testów, a wkrótce będziesz się zastanawiać, jak kiedykolwiek tworzyłeś oprogramowanie w inny sposób.