Du pushst vor der Mittagspause eine kleine UI-Änderung. Sie wirkt harmlos: Ein Button-Label ändert sich, ein Conditional Render wird vereinfacht und ein Helper-Hook bekommt einen neuen Branch. Der Pull Request ist sauber, das Review geht schnell und das Deployment läuft durch.
Eine Stunde später meldet der Support, dass der Login auf einer Plattform nicht mehr funktioniert. Web sieht gut aus. Die Desktop-Shell hat einen veralteten Render-Pfad. Der Mobile-Build verhält sich nach einem async State Change anders. Niemand hat es bemerkt, weil code zwar Tests hatte, aber nicht die richtigen Tests – und definitiv kein zuverlässiges System drumherum.
Das ist das Hauptproblem beim React-Unit-Testing in Produktionsteams. Ein paar Tests zu schreiben, die durchlaufen, ist nicht schwer. Eine Suite aufzubauen, die dich bei Refactors, Release Trains, Hotfixes und Cross-Platform-Packaging wirklich schützt, ist die eigentliche Herausforderung. React-Apps scheitern nicht, weil ein Team vergessen hat, wie man render()aufruft. Sie scheitern, weil Tests zu Implementierungsdetails abdriften, async Verhalten kaschiert wird und die CI Testing als Checkbox statt als Release Gate behandelt.
Modernes Unit Testing in React funktioniert wie ein Sicherheitssystem: Schnelles lokales Feedback, deterministische Checks in der CI und klare Grenzen zwischen Unit Tests und anderen Tests. Das ist besonders wichtig, wenn derselbe React-Code über Browser, Capacitor Container oder Electron-Shells ausgeliefert wird.
Inhaltsverzeichnis
- Warum Unit Testing in React dein bestes Sicherheitsnetz ist
- Einrichten deiner modernen React-Testing-Umgebung
- Sinnvolle Component Tests schreiben
- Custom Hooks und App-Logik testen
- Fortgeschrittene Techniken: Mocking und Async
- Testqualität und Strategie verbessern
- Tests in eine Cross-Platform CI/CD Pipeline integrieren
Warum Unit Testing in React dein bestes Sicherheitsnetz ist
Unit Tests sind dann wertvoll, wenn sie genau den Fehler finden, von dem du dachtest, er könne nicht passieren. In React bedeutet das meist, dass eine Komponente zwar noch rendert, aber das Verhalten für den User nicht mehr stimmt. Ein deaktivierter Button ist plötzlich klickbar. Ein Loading-State verschwindet nie. Eine Fallback-Message ist nach einem Refactor weg. Solche Fehler sind in code klein, aber in der Production teuer.
Das React-Testing hat sich entscheidend verändert, als die React Testing Library zum Standard wurde, um Verhalten statt Internals zu testen, was Teams dazu bewegt, Tests zu schreiben, die das Nutzerverhalten widerspiegeln statt Props oder State – wie auch in der React Native Testing Guidance unter React Native Testing Overviewbeschrieben. Dieser Shift ist wichtig, weil React code ständig umstrukturiert wird. Hooks wandern. Komponenten werden gesplittet. Context wird eingeführt. Ein Test, der an die interne Struktur gebunden ist, bricht bei jedem Refactor. Ein Test, der am sichtbaren Verhalten ansetzt, übersteht ihn meist problemlos.
Was ein Unit Test absichern sollte
Ein guter React Unit Test schützt einen kleinen Contract:
- Gerenderter Output: Sieht der User den richtigen Text, das Label, den State oder den Fallback?
- Interaktionsverhalten: Ändern Klicks, Eingaben oder Toggles die UI korrekt?
- Boundary Handling: Verhält sich die Komponente korrekt, wenn sie erwartete Inputs, fehlende Daten oder einen Error-Pfad erhält?
Ein schwacher Test schützt das Falsche:
- Komponenten-Internals: State-Struktur, private Methoden, reine Implementierungs-Props
- Framework-Mechanik: Ob React einen Hook intern exakt so aktualisiert hat, wie erwartet
- Details der Child-Komponenten: Markup, das zu verschachtelten Komponenten gehört, die du hier nicht prüfen möchtest
Praktische Regel: Wenn du die Komponente refactoren kannst, ohne die User Experience zu ändern, sollte sich auch der Test nicht ändern müssen.
Unit Tests sind zudem Teil eines größeren Testsystems. Sie versuchen nicht, die gesamte App End-to-End zu beweisen. Sie sind die schnelle Ebene, die Regressions abfängt, bevor du Browser-Tests oder Validierungen auf Device-Ebene benötigst. Deshalb sind sie die erste Verteidigungslinie in jedem vernünftigen Stack für automatisierte Tests von Production-Apps.
Für React-Teams mit schnellen Release-Zyklen schafft diese Arbeitsteilung Sicherheit. Unit Tests finden lokale Regressions schnell. Integration Tests prüfen die Schnittstellen. End-to-End Tests bestätigen die kritischen Pfade. Überspringst du die Unit-Ebene, muss alles nachgelagerte, langsamere Testing zu viel Last tragen.
Setup deiner modernen React-Testing-Umgebung
Eine instabile Testumgebung führt zu Flaky Tests, noch bevor du die erste Assertion geschrieben hast. Viele Entwickler geben Jest, jsdom oder React die Schuld, obwohl das eigentliche Problem inkonsistente Konfigurationen zwischen lokalen Maschinen und der CI sind. Die Lösung: Mach die Umgebung „langweilig“. Langweilig ist hier ein Kompliment.

Starte mit einem vorhersehbaren Runner und Environment
Für eine moderne React-App, besonders wenn sie mit Vite erstellt wurde, sollte das Basis-Setup Folgendes enthalten:
- Ein Test Runner: Jest ist nach wie vor verbreitet, besonders in älteren React-Codebases und Enterprise-CI-Stacks.
- Eine browserähnliche Umgebung:
jsdomermöglicht es Component-Tests, DOM-Output zu rendern. - Testing Library Utilities:
@testing-library/reactund@testing-library/jest-dom - Ein einziger Setup-Entrypoint: Eine Datei zum Registrieren von Matchern und globalen Mocks
Der zentrale Workflow, den Reacts Testing-Guidance betont, ist simpel: Komponente in einer jsdom-basierten Umgebung rendern, UI mit Selectoren wie getByText oder getByRoleabfragen, Interaktionen auslösen und die DOM-Änderung prüfen, wie in der React-Testing-Dokumentationbeschrieben. Dieser Workflow ist nur dann zuverlässig, wenn jede Maschine dieselbe Testumgebung nutzt.
Ein praktisches Jest-Setup sieht meistens so aus:
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy',
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
};
Wenn dein Team SWC statt Babel nutzt, ist das völlig okay. Es geht nicht um den Transformer, sondern um Konsistenz. Entscheidet euch für einen Weg und standardisiert ihn im Repo. Wenn du eine gute Referenz für allgemeine JavaScript-Testing-Konventionen suchst, ist Capgos Leitfaden für JavaScript Unit Tests ein nützliches Dokument für das Team-Handoff.
Füge die Setup-Datei hinzu, von der deine Suite abhängt
Ein ordentliches setupTests.js spart viel unnötigen Noise:
import '@testing-library/jest-dom';
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
In dieser Datei löst du Umgebungslücken einmalig, statt in zwanzig einzelnen Testdateien. Füge Mocks für APIs hinzu, auf die deine UI angewiesen ist, wie zum Beispiel matchMedia, ResizeObserver, oder IntersectionObserver, falls deine Component Library sie erwartet.
Ohne dies werden Entwickler Globals ad hoc patchen. Das führt zu inkonsistenten Tests und schwer nachvollziehbaren Fehlern. Der lokale Run eines Entwicklers läuft, weil er einen manuellen Mock in einer Datei hinzugefügt hat. Die CI schlägt fehl, weil das Setup nicht geteilt wurde.
Lokales Verhalten und CI synchron halten
Der lokale Befehl sollte dem CI-Befehl so nah wie möglich sein. Wenn Entwickler den Watch-Mode mit lockeren Defaults nutzen, die CI aber eine striktere Config verwendet, kommt es nach dem Merge zu unerwarteten Fehlern. Haltet Scripts explizit:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:ci": "jest --runInBand --coverage"
}
}
Ein kurzer Walkthrough hilft neuen Teammitgliedern, schnell das gleiche Baseline-Level zu erreichen:
Die wichtigste Entscheidung beim Setup ist die Disziplin bei den Defaults. Nutzt Aliases in der Config. Packt Environment-Mocks in eine zentrale Setup-Datei. Nutzt jsdom für UI-Tests und – wenn möglich – eine schlankere Umgebung für reine Utilities. Je weniger individuelles Custom-Verhalten jeder einzelne Test benötigt, desto zuverlässiger wird euer System.
Sinnvolle Component-Tests schreiben
Unternehmen haben kein Problem damit, Tests zu schreiben. Ihr Problem ist, Tests zu schreiben, die auch in sechs Monaten noch relevant sind.
Das Standard-Pattern für Unit-Tests von React-Komponenten ist nach wie vor das richtige: Komponente rendern, UI mit benutzerzentrierten Selectoren abfragen, eine Interaktion auslösen und die resultierende DOM-Änderung prüfen, wodurch die Tests fern von Implementierungsdetails wie State oder Props bleiben, wie im React Testing Guidebeschrieben. Der Trick besteht darin, dieses Pattern mit Bedacht anzuwenden.
Testet das Accordion so, wie ein User es bedienen würde
Nehmen wir eine einfache Accordion Komponente. Sie rendert einen Button mit einem Titel. Der Panel-Inhalt ist anfangs ausgeblendet. Ein Klick auf den Button zeigt den Inhalt und aktualisiert den Accessibility-Status.
Das reicht als Verhalten für mehrere nützliche Tests:
- Der erste Render zeigt den Titel, aber nicht den Inhalt.
- Ein Klick auf den Trigger zeigt den Inhalt.
- Ein erneuter Klick klappt ihn wieder ein.
- Die Accessibility-Attribute spiegeln den sichtbaren Zustand wider.
Der letzte Punkt wird oft übersprungen. Wenn deine Komponente aria-expanded, aria-controlsoder eine rollenbasierte Struktur nutzt, prüfe sie. Das sind keine Implementierungsdetails, sondern Teil des Vertrags nach außen.
Die besten Komponententests lesen sich wie ein Bugreport, den man nie bekommen will.
Queries nach Intent wählen
Die React Testing Library bietet verschiedene Query-Styles, die jedoch nicht austauschbar sind. Die falsche Wahl macht Tests unübersichtlich oder irreführend.
| Abfragetyp | Wenn Element gefunden wird | Wenn Element nicht gefunden wird | Anwendungsbeispiel |
|---|---|---|---|
getBy |
Gibt das Element sofort zurück | Wirft sofort einen Fehler | Prüfen, ob Button oder Heading bereits sichtbar ist |
queryBy |
Gibt das Element sofort zurück | Rückgabewert null |
Prüfen, ob versteckter Inhalt vor Interaktion nicht existiert |
findBy |
Wird aufgelöst, wenn das Element erscheint | Wird nach Wartezeit abgelehnt | Prüfen, ob async geladene Inhalte nach einem Fetch oder verzögerten Update erscheinen |
Ein einfaches mentales Modell hilft dabei:
- Nutze
getByfür Dinge, die bereits existieren müssen. - Nutze
queryByfür Dinge, die noch nicht existieren dürfen. - Nutze
findBywenn sich die UI später ändert.
Wenn ein Test mit beginnt, findBy für alles, bedeutet das meist, dass der Autor unsicher ist, wann die Komponente aktualisiert wird. Diese Unsicherheit führt später zu Flakiness.
Ein praktisches Accordion-Beispiel
Hier ist eine beispielhafte Komponente:
function Accordion({ title, children }) {
const [open, setOpen] = React.useState(false);
return (
<section>
<button
aria-expanded={open}
aria-controls="accordion-panel"
onClick={() => setOpen(prev => !prev)}
>
{title}
</button>
{open ? (
<div id="accordion-panel">
{children}
</div>
) : null}
</section>
);
}
So sehen Tests aus, die sich wirklich lohnen:
import { render, screen, fireEvent } from '@testing-library/react';
test('renders the accordion title and hides content initially', () => {
render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);
expect(screen.getByRole('button', { name: /shipping details/i })).toBeInTheDocument();
expect(screen.queryByText(/delivery takes 3 days/i)).not.toBeInTheDocument();
});
test('reveals content when the trigger is clicked', () => {
render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);
fireEvent.click(screen.getByRole('button', { name: /shipping details/i }));
expect(screen.getByText(/delivery takes 3 days/i)).toBeInTheDocument();
});
test('updates aria-expanded when opened', () => {
render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);
const button = screen.getByRole('button', { name: /shipping details/i });
expect(button).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(button);
expect(button).toHaveAttribute('aria-expanded', 'true');
});
Was fehlt, ist genauso wichtig. Es gibt keine Assertions für den internen State. Keine Prüfung, ob setOpen aufgerufen wurde. Kein Snapshot des gesamten Render-Trees. Solche Tests würden nur Wartungsaufwand statt Vertrauen schaffen.
Ein paar Gewohnheiten machen Komponententests stärker:
- Bevorzuge role-basierte Queries: Buttons, Headings, Dialogs, Alerts und Inputs sollten normalerweise über ihre Role gefunden werden.
- Halte jeden Test fokussiert: Ein nutzerbares Verhalten pro Test sorgt dafür, dass Fehler lesbar bleiben.
- Benenne Tests nach dem Ergebnis: „setzt aria-expanded beim Öffnen“ ist viel hilfreicher als „funktioniert korrekt“.
Ist eine Komponente über das DOM schwer zu testen, deutet das oft auf ein Designproblem hin. Vielleicht wird der State am falschen Ort verwaltet oder es fehlt semantisches Markup. Gute Tests führen Teams oft zu besseren Komponenten.
Testing von Custom Hooks und App-Logik
React-Apps verbergen viel wichtiges Verhalten außerhalb von Komponenten. State-Transitions liegen in Hooks. Validierung und Formatierung in Helper-Funktionen. Data Shaping passiert oft, bevor etwas gerendert wird. Wenn du nur sichtbare Komponenten testest, verpasst du einen großen Teil von code – und riskierst trotzdem Brüche im Production-Verhalten.
Hooks brauchen ein React-aware Test-Harness
Ein Custom Hook braucht weiterhin React für die korrekte Ausführung, teste ihn also mit renderHook und umschließe State-ändernde Aufrufe mit act().
Ein kleiner useToggle Hook ist ein gutes Beispiel:
import { useState, useCallback } from 'react';
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(current => !current), []);
return { value, toggle };
}
Der Test sollte sich auf den öffentlichen Contract konzentrieren:
import { renderHook, act } from '@testing-library/react';
import { useToggle } from './useToggle';
test('returns the initial value', () => {
const { result } = renderHook(() => useToggle(true));
expect(result.current.value).toBe(true);
});
test('toggles the value', () => {
const { result } = renderHook(() => useToggle(false));
act(() => {
result.current.toggle();
});
expect(result.current.value).toBe(true);
});
Dieser Test ist nützlich, weil der Hook selbst die Unit ist. Du testest nicht die React-Internals, sondern verifizierst das externe Verhalten des Hooks.
Für Produktteams, die wiederverwendbare UI- oder Feature-Primitives bauen, ist dieses Pattern sehr wichtig. Hooks werden oft zur Schnittstelle, die Apps, Design-Systeme oder internes Tooling teilen. Wenn du wiederverwendbares Verhalten mit kommerziellem Fokus entwickelst, können Ressourcen zu Hooks für Maker-Produkte dabei helfen, Hooks als produktisierte Bausteine statt nur als Implementierungsdetails zu betrachten.
Pure Logic sollte in Tests pure bleiben
Nicht alles benötigt jsdom, React oder die Testing Library. Wenn eine Funktion pure ist, teste sie einfach mit Jest in einer Node-Umgebung.
Beispiel:
export function formatDisplayName(firstName: string, lastName: string) {
return `${firstName.trim()} ${lastName.trim()}`.trim();
}
Dieser Test sollte denkbar einfach sein:
import { formatDisplayName } from './formatDisplayName';
test('joins and trims both names', () => {
expect(formatDisplayName(' Ada ', ' Lovelace ')).toBe('Ada Lovelace');
});
test('handles a missing last name', () => {
expect(formatDisplayName('Ada', '')).toBe('Ada');
});
Der Vorteil liegt in Geschwindigkeit und Klarheit. Wenn eine Funktion keinen Render-Tree braucht, gib ihr keinen. React-spezifische Tools verursachen Overhead. Halte Business-Logic-Tests klein, schnell und nah an der zu prüfenden Funktion.
Eine praktische Aufteilung sieht so aus:
- Hooks: Nutze
renderHook,act(), und Wrapper-Provider bei Bedarf. - Utilities: Nutze einfaches Jest ohne DOM.
- Stateful Cross-cutting Logic: Lagere Logik in testbare Helfer aus, wenn der Komponententest zu viel übernimmt.
Teams überladen Komponententests oft mit Logik-Assertions, die tiefer im Stack hingehören. Das Auslagern bietet zwei Vorteile: Der Komponententest wird sauberer und der Logiktest wird schneller.
Fortgeschrittene Techniken: Mocking und Async meistern
Die meisten unzuverlässigen React-Suites scheitern an zwei Stellen: an den Abhängigkeitsgrenzen und an zeitlichen Abläufen.
Deshalb sind Async-Testing und Mocking die Trennlinie zwischen einer Spielerei und einer Testsuite, der man vor dem Release vertrauen kann. Eine Analyse schreibt 46,5 % der Test-Flakiness auf umgebungs- oder ressourcenbedingte Probleme wie Async-Timing zurück in dieser React Unit Testing Analyse. In React-Apps bedeutet das direkt: State-Transitions, verzögertes Rendering, netzwerkgesteuerte UIs und Tests, die raten, statt deterministisch zu warten.

Mock die Grenze, nicht jede Ebene
Der schnellste Weg zu einem irreführenden Test: Mocke die Hälfte deines Component-Trees und assertiere dann, dass deine eigenen Mocks funktioniert haben.
Mocke bei einer Komponente, die Kontodaten abruft, den Network-Client oder das API Modul. Mocke nicht den Hook, die Child-Row-Komponente, den Loading-Spinner oder drei Utility-Funktionen, es sei denn, der Test benötigt echte Isolation an diesen Stellen.
Nutze diesen Regelsatz:
- Externe Services mocken: HTTP-Clients, Analytics, Browser-only APIs, Native Bridges
- Instabile Platform-APIs mocken:
matchMedia, Timer, Electron Preload-Interfaces, Capacitor Plugins, wenn sie in jsdom nicht verfügbar sind - Vermeide standardmäßig das Mocken interner Logik: Custom Hooks, einfache Child-Komponenten, lokale Utilities
Wenn ein Test nur besteht, weil alle schwierigen Teile durch Fakes ersetzt wurden, bringt er dir kaum Sicherheit für das Release.
Für Teams, die Beispiele und Patterns für Runner-APIs suchen, ist Capgo Testing-Tutorials eine praktische Referenzbibliothek, besonders beim Onboarding von Entwicklern, die React kennen, aber noch keine Testing-Mechaniken beherrschen.
Async-Tests schlagen bei ungenauem Timing fehl
Async-Fehler entstehen meist durch einen dieser drei Fehler:
- Der Test führt das Assertion zu früh aus.
- Der Test wartet mit willkürlichen Timern.
- Die Komponente aktualisiert sich mehrfach, aber der Test bildet nur einen Übergang ab.
Ein stabiler Async-Test hat meist diese Struktur:
test('shows user details after data loads', async () => {
render(<UserProfile userId="42" />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
expect(await screen.findByText(/account owner/i)).toBeInTheDocument();
});
Oder wenn du auf eine bestimmte Bedingung warten musst:
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
});
Nutze findBy wenn das Erscheinen eines Elements das entscheidende Event ist. Nutze waitFor wenn die Bedingung allgemeiner ist oder der State nicht mit einer einzelnen Query abgebildet werden kann. Vermeide setTimeout in Tests, es sei denn, du testest explizit das Timer-Verhalten mit Fake Timern.
Das React-Testing-Ökosystem erwartet zudem, dass du act() Semantik von Updates. Die Testing Library übernimmt vieles davon, aber wenn du State manuell steuerst oder Timer vorziehst, musst du darauf achten, wann Updates geflasht werden.
Das richtige Mocking-Tool wählen
Verschiedene Mocking-Tools lösen unterschiedliche Probleme:
| Tool | Beste Nutzung | Häufiger Fehler |
|---|---|---|
jest.fn() |
Standalone Fake-Callbacks oder injizierte Funktionen | Es als Ersatz für ein ganzes Modul verwenden, wenn ein einfacher Callback ausreichen würde |
jest.spyOn() |
Eine Methode auf einem echten Objekt oder Modul beobachten oder überschreiben | Vergessen, die ursprüngliche Implementierung wiederherzustellen |
jest.mock() |
Modul-Abhängigkeiten an der Import-Grenze ersetzen | Große Module standardmäßig mocken und dabei sinnvolles Verhalten verlieren |
Beispiele helfen:
- Greifen Sie zu
jest.fn()wenn eine Komponente eineonSubmitProp erhält. - Nutze
jest.spyOn()wenn Sie verifizieren müssen,console.error, eine Speicher-Methode oder einen exportierten API Aufruf. - Nutze
jest.mock()wenn ein Modul-Import sonst I/O, natives code oder Verhalten außerhalb der Unit-Grenze auslösen würde.
Ein fortgeschrittener Bereich, der in vielen Guides zu kurz kommt, ist das Testen von Fehlerpfaden in modernem React. Error Boundaries, verzögerte State-Änderungen und asynchrone Fallback-UIs verdienen erstklassige Tests, nicht nur das typische „Happy Path“-Klickbeispiel. Wenn ein Child einen Fehler wirft, prüfen Sie das Fallback-UI. Wenn eine Anfrage fehlschlägt, prüfen Sie den sichtbaren Recovery-State. Wenn ein Button während des Ladens deaktiviert ist, prüfen Sie auch das. Das sind die Bugs, an die sich User erinnern.
Testqualität und Strategie verbessern
Viele Teams jagen immer noch der Coverage hinterher, als wäre sie dasselbe wie Vertrauen. Das ist sie nicht.
Man kann ein Coverage-Ziel erreichen und trotzdem die wichtigen Regressionen übersehen. Eine Suite voller oberflächlicher Assertions, breiter Snapshots und gemockter Internals erzeugt den Anschein von Sicherheit und erhöht zugleich die Wartungskosten.

Coverage ist eine Karte, nicht das Ziel
Coverage-Reports sind dann nützlich, wenn sie eine Frage beantworten: Welche kritischen Pfade sind noch ungeschützt?
Sie sind nicht nützlich, wenn sie Entwickler dazu drängen, triviale Wrapper, statisches Markup oder einzeilige Durchreich-Dateien zu testen, nur um eine Prozentzahl zu steigern. Nutze Coverage als Discovery-Tool. Wenn Auth-State, Billing-Aktionen, Feature Flags oder Update-Prompts keine Tests haben, ist das ein Signal. Wenn eine Presentational-Icon-Komponente keine Tests hat, ist das meistens kein Signal.
Eine gute Review-Frage ist simpel: Reduziert dieser Test das Release-Risiko?
- Ja: Er verifiziert nutzersichtbares Verhalten auf einem kritischen Pfad.
- Vielleicht: Er schützt Business-Logik, die beim Refactoring leicht kaputtgehen kann.
- Nein: Er prüft Implementierungsdetails oder dupliziert den Wert eines anderen Tests.
Was man nicht unit-testen sollte
Viele React-Guides gehen noch immer nicht genug auf das Thema Auslassungen ein. Diese Lücke ist wichtig, da Over-Mocking und das Testen von Implementierungsdetails zu instabilen Test-Suites führen, die zwar bestehen, während die User Experience trotzdem fehlerhaft bleibt – wie auch im BrowserStack-Guide zu was man in React nicht unit-testen sollte.
Vermeide oder limitiere diese Patterns:
- Assertions über den internen State: Teste nicht
isOpendirekt, wenn du stattdessen prüfen kannst, ob das Panel geöffnet wurde. - Framework-Verhalten: Teste nicht, ob React einen Effect aufgerufen hat. Teste das Ergebnis dessen, was der Effect verändert.
- Interna von Third-Party-Libraries: Teste deine Integration mit einem Datepicker oder Router, nicht die Rendering-Logik der Library selbst.
- Zu stark zerlegte Units: Wenn du jedes Child und jeden Helper mockst, testest du am Ende vielleicht gar kein sinnvolles Verhalten mehr.
Schlechte Tests sind schlimmer als gar keine Tests, wenn sie Refactorings blockieren und trotzdem Bugs in der Produktion übersehen.
Eine hilfreiche Heuristik ist die Ownership der Grenzen. Teste das, was dein code kontrolliert. Teste nicht, was React, der Browser oder eine etablierte Library bereits übernimmt, es sei denn, dein Integration Layer ändert den Contract.
Wo Snapshots helfen und wo sie schaden
Snapshots sind nicht nutzlos. Man kann sie nur leicht falsch einsetzen.
Nutze sie sparsam für Komponenten mit stabilem, einfachem Output, bei denen ein struktureller Diff sinnvoll ist. Vermeide sie bei interaktiven oder hochdynamischen Komponenten, da sie sonst nur Rauschen erzeugen. Entwickler hören auf, sie zu lesen, und aktualisieren sie nur noch reflexartig.
Meistens gibt es bessere Alternativen:
- Nutze für Conditional Rendering Assertions auf das Vorhandensein oder Fehlen von wichtigem Text.
- Nutze für visuelle State-Änderungen Assertions auf die relevante Role, das Label oder das Attribut.
- Nutze für Errors und Fallbacks Assertions auf die tatsächliche Message oder die Alert-Region.
Wenn dein Team einen umfassenderen Qualitätsprozess über Unit Tests hinaus benötigt, ist ein solider Begleiter ein App Quality Assurance Workflow ein System, das Tests, Release-Checks und Rollback-Planung als Einheit betrachtet. Das ist der entscheidende Mindset-Shift für die schnellste Verbesserung der Testqualität. Hör auf zu fragen, wie viele Tests du hast. Frag stattdessen, welche Fehler deine Nutzer noch erreichen könnten.
Integration von Tests in eine Cross-Platform CI/CD-Pipeline
Eine Testsuite, die nur auf dem Laptop eines Entwicklers läuft, ist lediglich eine Empfehlung, keine Kontrolle.
Die Suite wird erst dann einsatzbereit, wenn jeder Pull Request dieselben Checks in einer sauberen Umgebung ausführt und Merges bei Fehlern blockiert. Das klingt logisch, aber viele Teams lassen kritische Lücken. Tests laufen manuell. Coverage-Reports sind optional. Packaging- und Release-Jobs starten, bevor die Test-Jobs fertig sind. So schlüpfen kleine UI-Regressions in größere Release-Fehler.

Ein Pull Request sollte jedes Mal denselben Gate triggern
Damit Unit Testing in React wie ein Sicherheitsnetz funktioniert, benötigt die CI einige Essentials:
- Bei jedem Pull Request ausführen
- Dependencies aus dem Lockfile installieren
- Immer denselben Test-Command nutzen
- Bei Testfehlern sofort abbrechen (Fail fast)
- Artifacts erst nach erfolgreichen Tests veröffentlichen
Das ist der Kern von Continuous-Deployment-Praktiken für App-Teams. Schafft Vertrauen vor dem Release, nicht danach.
Ein einfacher GitHub Actions Workflow reicht für viele Teams aus:
name: test
on:
pull_request:
push:
branches:
- main
jobs:
react-tests:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:ci
Das ist nicht kompliziert, und genau das ist der Punkt. Die stabilsten Pipelines sind meist die, die am wenigsten überraschen.
Warum das für Capacitor und Electron noch wichtiger ist
Cross-Platform React Apps bergen ein höheres Release-Risiko als reine Browser-Apps, da dieselbe UI code oft in verschiedenen Containern mit unterschiedlichen Runtime-Annahmen ausgeliefert wird.
Ein paar Beispiele zeigen, wo Pipelines helfen:
- Capacitor Apps: Web code funktionieren lokal vielleicht, scheitern aber, wenn ein Plugin-Bridge, der Offline-Status oder Edge-Cases im App-Lifecycle nach dem Packaging das Verhalten ändern.
- Electron Apps: Eine Renderer-Komponente kann von Preload-APIs, Window-Messaging oder Desktop-spezifischem State abhängen, die bei einem reinen Browser-Test nicht vorhanden sind, sofern sie nicht gezielt gemockt werden.
- Gemeinsame Release-Trains: Ein fehlerhaftes Bundle kann mehrere Targets beeinträchtigen, wenn der Deployment-Prozess die Veröffentlichung nicht strikt kontrolliert.
Deshalb sollten Unit Tests vor den Packaging-Jobs laufen und Packaging-Jobs vor den Distribution-Jobs. Jede Stufe minimiert das Risiko. Unit Tests finden lokale Regressions schnell. Das Plattform-Packaging prüft Umgebungsvoraussetzungen. Manuelle Freigaben oder Staged Rollouts sorgen für die finale Sicherheit beim Release.
Ein praktischer GitHub Actions Workflow
Eine ausgereifte Pipeline trennt meist die Verantwortlichkeiten:
- Test-Job: Schnelle Unit- und Hook-Tests
- Build-Job: Production Build erst nach erfolgreichen Tests
- Package-Job: Capacitor Sync, Electron-Packaging oder Artifact-Bundling
- Release-Job: Veröffentlichung nur über freigegebene Branches oder Tags
Für Teams, die Live Updates für Capacitor oder Electron-Apps ausrollen, ist das Release-Tooling entscheidend. Eine Option in diesem Workflow ist Capgo, das signierte Web-Bundles für CapacitorJS- und Electron-Apps mit Rollback-Support und kanalbasierten Rollout-Steuerungen veröffentlicht. In der Praxis bedeutet das, dass dein React-Testjob als erste harte Barriere fungieren kann, bevor ein Web-Bundle für Staging oder Production freigegeben wird.
Die operative Regel ist simpel: Lass die Release-Infrastruktur nicht Schwächen in den Tests ausgleichen. Nutze die Release-Infrastruktur erst, nachdem zuverlässige Tests fehlerhafte Änderungen bereits aussortiert haben.
Ein verlässliches Testsystem verändert das Teamverhalten. Engineers mergen mit weniger Zögern. Reviewer konzentrieren sich auf Edge Cases, statt Basics manuell zu prüfen. Release Manager hören auf, jedes Deployment wie ein Glücksspiel zu behandeln. Das ist das Ergebnis von gutem Unit Testing in React.
Wenn dein Team React über Capacitor oder Electron ausrollt, hängt die Release-Sicherheit von mehr ab als nur von grünen lokalen Tests. Capgo bietet Teams eine kontrollierte Möglichkeit, signierte Web-Updates zu veröffentlichen, Rollout-Kanäle zu targeten und fehlerhafte Bundles ohne Store-Review zurückzurollen – ideal integriert in eine CI-Pipeline, die vor dem Deployment bereits bestandene Unit Tests voraussetzt.