Zum Inhalt
wannacode

Komponenten testen

Kurz gesagt: Komponententests mit Testing Library und Vitest prüfen, was ein Mensch sieht: Sie finden Elemente nach Rolle, Label und Text und klicken wie ein Benutzer.

Problem

Der Test ist grün, der Button ist kaputt

Jemand hat einen Test für die Produktkarte geschrieben. Führ ihn aus.

React

Der Bericht sagt „Tests: 1 bestanden, 0 fehlgeschlagen“. Öffne jetzt ProductCard.jsx: Der Button hat kein onClick. Der Kunde drückt „In den Warenkorb“, und nichts passiert, und der Test ist grün.

Der Test findet den Button über seine Klasse (.add) und prüft seinen Text. Niemand drückt ihn. Auch das Gegenteil passiert: Benenne die Klasse in btn-add um, und der Test schlägt fehl, während der Mensch vor dem Bildschirm keinen Unterschied sieht. Der Test folgt der Art, wie der Code geschrieben ist, nicht dem, was er tut.

Die Idee

Teste, was ein Mensch sieht

Wenn du im JavaScript-Kurs Tests geschrieben hast: Die Idee ist dieselbe, Code, der deinen Code aufruft und das Ergebnis vergleicht. Der Unterschied ist, dass das Ergebnis einer Komponente eine Seite ist. Also tut der Test, was ein Mensch davor tut:

  1. Zeichne die Komponente: render.
  2. Finde das Element so, wie ein Mensch es findet: nach Rolle und Name, nach Label, nach Text.
  3. Handle wie ein Mensch: user.click, user.type.
  4. Prüfe, was sichtbar ist: expect(…).toBeInTheDocument().

Es gibt zwei Werkzeuge. Vitest führt die Tests aus (test, expect), und Testing Library zeichnet die Komponente und durchsucht die Seite. Das sind Komponententests.

import { render, screen } from '@testing-library/react';
render(<ProductCard product={bread} />);
screen.getByRole('button', { name: 'In den Warenkorb' });

Die Reihenfolge, in der Testing Library dir zum Suchen rät:

  • getByRole mit einem Namen: ein Button, ein Link, eine Überschrift, ein Feld. So sucht ein Screenreader, deshalb prüft der Test auch die Barrierefreiheit. Ein Button ohne Namen wird nicht gefunden.
  • getByLabelText: ein Feld über sein Label.
  • getByText: Text, der nicht in einem Button oder einer Überschrift steht.
  • getByTestId erst am Ende, wenn nichts anderes funktioniert. Das Attribut data-testid ist für den Menschen nicht sichtbar.

Sieh es dir an

Sieh, wie der Test läuft

Die vier Schritte an einer Karte. Danach derselbe Test gegen eine defekte Version.

test('der Button legt das Produkt in den Warenkorb', async () => {
const user = userEvent.setup();
render(<ProductCard product={bread} />);
const button = screen.getByRole('button', {
name: 'In den Warenkorb',
});
await user.click(button);
expect(screen.getByText('Im Warenkorb: 1'))
.toBeInTheDocument();
});
Schritt für Schritt, in Worten
  1. test() gibt einer Prüfung einen Namen. Vitest führt jeden Test in der Datei aus und meldet, welche bestanden haben.
  2. render zeichnet ProductCard in eine leere Seite, wie es der Browser täte. Keine App, kein Rest des Shops: nur diese Komponente.
  3. Der Test sucht den Button wie ein Mensch: die Rolle „button“ und den Namen „In den Warenkorb“. Die Klasse und die Stelle im Code spielen keine Rolle.
  4. user.click drückt den Button wie ein Mensch: Zeiger-Ereignisse, dann click. Die Komponente aktualisiert sich und zeigt „Im Warenkorb: 1“.
  5. expect prüft, was der Mensch sieht: Der Text steht auf der Seite. Der Test besteht.
  6. Jetzt derselbe Test gegen eine defekte Version: Der Button ist an nichts angeschlossen. Der Klick erreicht den Button, und nichts ändert sich.
  7. expect findet „Im Warenkorb: 1“ nicht, und der Test schlägt fehl. Grün beim richtigen Code und rot beim defekten: Auf einen solchen Test kannst du dich verlassen.

Der Test weiß nicht, ob der Code ein className hat, wie viele <div> um den Button stehen oder wie die Funktion heißt. Er weiß, was auf dem Bildschirm steht. Deshalb übersteht er ein Refactoring und schlägt bei einem Fehler fehl.

Probier es aus

Der erste echte Test

Zwei Tests: Die Karte zeigt Namen und Preis, und der Button legt in den Warenkorb. Führ sie aus. Entferne dann onClick={add} in ProductCard.jsx und führ noch einmal aus: Der zweite Test schlägt fehl.

React

userEvent.setup() erzeugt einen „Benutzer“, und await user.click(…) klickt wie ein Mensch: Der Zeiger bewegt sich über den Button, drückt, lässt los, und erst dann kommt click. Jede Aktion von user gibt ein Promise zurück, deshalb ist der Test async und wartet mit await.

toBeInTheDocument() kommt von jest-dom: Prüfungen für die Seite wie toBeDisabled(), toHaveTextContent(), toHaveValue(). In der Sandbox sind sie eingebaut, und in einem Projekt schaltest du sie mit einer Zeile ein (siehe „Genauer, für Neugierige“).

Probier es aus

Finde den Fehler

Der Test schlägt mit „Unable to find an element with the text: Im Warenkorb: 1“ fehl, und die Komponente ist die richtige. Welche Zeile ist schuld?

Wähle die Zeile mit dem Fehler.

Die Idee

Was die Karte dem Elternelement sagt: vi.fn()

Die Karte besitzt den Warenkorb nicht. Sie sagt dem Elternelement „füge dieses Produkt hinzu“ (die Lektion über gemeinsamen Zustand). Ein Test der Karte sollte nicht den ganzen Shop zeichnen, um das zu sehen. Statt eines Elternelements übergibst du vi.fn(): eine leere Funktion, die sich jeden Aufruf merkt. Eine solche Stellvertreter-Funktion heißt Mock.

const onAdd = vi.fn();
render(<ProductCard product={bread} onAdd={onAdd} />);
await user.click(screen.getByRole('button', { name: 'In den Warenkorb' }));
expect(onAdd).toHaveBeenCalledTimes(1);
expect(onAdd).toHaveBeenCalledWith(bread);

not dreht die Prüfung um: expect(onAdd).not.toHaveBeenCalled() heißt „nie aufgerufen“.

Der Test prüft, dass die Karte genau dieses Produkt schickt. Was kommt in die Lücke?

test('der Button legt das Produkt in den Warenkorb', async () => {
  const user = userEvent.setup();
  const onAdd = vi.fn();
  render(<ProductCard product={bread} onAdd={onAdd} />);
  await user.click(screen.getByRole('button', { name: 'In den Warenkorb' }));
  expect(onAdd).(bread);
});

Probier es aus

Das ausverkaufte Produkt

Die Banitsa ist ausverkauft: Der Button muss deaktiviert sein, die Karte muss „Nicht auf Lager“ sagen, und ein Klick darf onAdd nicht aufrufen. Vervollständige den Test: Klick auf den Button und prüfe alle drei. „Prüfen“ führt deinen Test gegen die richtige Karte aus und danach gegen zwei defekte Versionen. Der Test muss bei der richtigen bestehen und bei jeder defekten fehlschlagen.

React

Die Idee

Daten, die später kommen: findBy

Die Liste lädt die Produkte aus der API. Zuerst sagt sie „Lädt…“, und die Produkte kommen einen Moment später. Der Test muss auf sie warten. Führ die zwei Tests aus: Der erste schlägt absichtlich fehl.

React

Es gibt drei Arten von Abfragen:

  • getBy… sucht sofort. Wenn das Element nicht da ist, schlägt der Test fehl. Der erste Test schlägt genau so fehl: Die Antwort ist noch nicht angekommen.
  • findBy… gibt ein Promise zurück und versucht es immer wieder, bis das Element erscheint. Erscheint es nicht rechtzeitig (in Testing Library standardmäßig nach 1 Sekunde), schlägt es fehl. Deshalb steht es mit await.
  • queryBy… gibt null zurück, wenn es das Element nicht gibt, ohne fehlzuschlagen. Damit prüfst du, dass etwas weg ist: expect(screen.queryByText('Lädt…')).toBeNull().

Wartest du auf etwas, das kein Element ist (eine Funktion, die nach der Anfrage aufgerufen wird), nimmst du waitFor: await waitFor(() => expect(onLoad).toHaveBeenCalled()). Es führt die Prüfung aus, bis sie besteht oder die Zeit abläuft.

Genauer, für Neugierige

In einem echten Projekt. Die Pakete: npm install -D vitest jsdom @testing-library/user-event @testing-library/jest-dom und @testing-library/react @testing-library/dom. In vite.config.js fügst du test: { environment: 'jsdom' } hinzu: Die Tests laufen in Node, und jsdom gibt ihnen eine Seite. Am Anfang der Tests (oder in einer Datei aus setupFiles) schreibst du import '@testing-library/jest-dom/vitest', um toBeInTheDocument() und die anderen zu bekommen. Die Tests führst du mit npx vitest aus, und ihre Dateien sind *.test.jsx oder *.test.js neben der Komponente.

Aufräumen zwischen den Tests. Testing Library entfernt nach jedem Test von selbst, was es gezeichnet hat, wenn afterEach global ist. In Vitest ist es nur mit test: { globals: true } global. Sonst rufst du cleanup() in afterEach auf. In der Sandbox bekommt jeder Test eine leere Seite.

fireEvent und user. fireEvent.click(button) sendet ein einziges click-Ereignis. user.click(button) geht durch alles, was ein Mensch tut: der Zeiger, der Fokus, das Drücken. Ein Element mit pointer-events: none, das ein Mensch nicht drücken kann, weigert sich user mit einem Fehler zu drücken, während fireEvent das Ereignis sendet. Deshalb ist user näher an der Wahrheit.

End-to-End-Tests. Ein Komponententest zeichnet ein Stück. Ein End-to-End-Test (zum Beispiel mit Playwright) startet die ganze App in einem echten Browser und geht wie ein Kunde durch sie hindurch: Katalog, Warenkorb, Bestellung. Er findet Fehler zwischen den Teilen, ist aber langsamer. Deshalb gibt es viele Komponententests und wenige End-to-End-Tests, für die wichtigsten Wege.

Was du nicht testest. Den Zustand im Inneren (added, soldOut), die Namen von Funktionen und Klassen. Sie ändern sich bei einem Refactoring. Du testest die Eingabe (Props, Klicks, die Antworten der API) und die Ausgabe (die Seite und die Aufrufe nach oben).

Die Sandbox. Hier sind vitest und Testing Library kleine Versionen mit denselben Namen: nur der Teil der API, den diese Lektion benutzt. findBy und waitFor warten bis zu 1 Sekunde, und ein Test wird nach 4 Sekunden abgebrochen. Der Testcode ist geschrieben wie in einem echten Projekt.

Übung

Tests für den Warenkorb

CartSummary zeigt die Zeilen („Weißbrot × 2“ mit einem Button „Entfernen“), die Summe („Gesamt: 4,00 €“) und „Der Warenkorb ist leer“, wenn er leer ist. Bei „Entfernen“ sagt es dem Elternelement, welche Zeile zu entfernen ist. Schreib Tests, die drei defekte Versionen finden: Die Summe ignoriert die Menge, „Entfernen“ schickt die erste Zeile statt der eigenen, und der leere Warenkorb sagt nichts. Die Prüfung verlangt, dass alle deine Tests beim richtigen Code bestehen und alle drei Fehler gefunden werden.

React

Test

Teste dich

1. Der Test muss den Button „In den Warenkorb“ finden. Welche Abfrage benutzt du zuerst?
2. Die Komponente lädt die Produkte aus der API. Der Test ruft direkt nach dem Rendern getByRole('heading', { name: 'Weißbrot' }) auf und schlägt fehl. Warum?
3. Dein Test besteht gegen den richtigen Code und auch gegen eine Version, in der der Button nicht verdrahtet ist. Was bedeutet das?
4. Warum übergibst du im Test von ProductCard onAdd = vi.fn()?
5. Du willst den ganzen Weg prüfen: Katalog, Warenkorb, Bestellformular, ein echter Server. Was für ein Test ist das?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·