Startseite / Artikel

Anti-Muster bei der React-Testung, die 2026 eine falsche Sicherheit vermitteln.

Lernen Sie zehn häufige Fehler bei der React-Testung – von Fehlern beim Einsatz von fireEvent bis hin zu Problemen bei der Simulation des Netzwerks –, die dazu führen können, dass fehlerhafte Funktionen trotz einer vollständig bestandenen Testsuite durchgehen.

Verlag: REACTAPP.TOP14 Min. Lesezeit3125 Wörter

Quellen: 10 React Testing Mistakes That Are Secretly Making Your Tests Useless in 2026

Ihre Testsuite zeigt eine vollständige Bestätigung – alle Prüfungen sind erfolgreich – und Sie liefern die Anwendung in die Produktion, ohne Bedenken zu haben. Doch dann füllt sich die Support-Queue: Der Bestellbutton funktioniert auf Mobilgeräten stillschweigend nicht, die Anmeldeseite wirft Fehler auf, wenn die API langsam antwortet, und Nutzer mit Tastatur können das Navigationsmenü überhaupt nicht nutzen. Nichts in Ihren Tests hat etwas davon erfasst. Was ist also passiert?

Bis 2026 hat sich der Unterschied zwischen einer Suite, die lediglich bestanden ist, und einer, die tatsächlich Ihre Anwendung schützt, durch den Einsatz von React 19 zusammen mit dem React Compiler und der Testing Library v16 noch weiter vergrößert. Im Folgenden finden Sie zehn Gewohnheiten, die dazu führen, dass Ihre Tests ausschauen, als wären sie gründlich, während Ihre Produktionsanwendung dennoch stillschweigend Fehler aufweist.

1. Testen von Implementierungsdetails statt des Benutzerverhaltens

Das ist der klassische Fehler bei React-Tests, und er wird immer noch ständig begangen. Eine Komponente wird gerendert, ihre Interna werden untersucht, und die Prüfungen überprüfen beispielsweise, ob useState aufgerufen wurde oder ein bestimmter Prop weitergegeben wurde. Damit wird nicht überprüft, was der Benutzer tatsächlich erlebt – es wird nur überprüft, wie der Entwickler sie zufällig gebaut hat.

// ❌ Useless: Testing internals
import { render } from '@testing-library/react';
import { LoginForm } from './LoginForm';
test('sets isSubmitting to true on submit', () => {
  const wrapper = render(<LoginForm />);
  const instance = wrapper.container.firstChild; // Reaching into internals

  // This tests React, not your app
  expect(instance).toHaveProperty('isSubmitting');
});

Ein solcher Test ist brüchig und sinnlos: Wenn man eine Variable umbennt, useState durch useReducer ersetzt oder die Logik in einen eigenen Hook auslagert, bricht der Test – obwohl sich am eigentlichen Anmeldeprozess nichts geändert hat. Er liefert keinerlei Hinweis darauf, ob eine Person sich anmelden kann.

// ✅ Useful: Testing behavior
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('submits the form with user credentials', async () => {
  const handleSubmit = vi.fn();
  render(<LoginForm onSubmit={handleSubmit} />);

  const user = userEvent.setup();

  await user.type(screen.getByLabelText(/email/i), 'test@example.com');
  await user.type(screen.getByLabelText(/password/i), 'secret123');
  await user.click(screen.getByRole('button', { name: /sign in/i }));

  expect(handleSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
    password: 'secret123',
  });
});

Das grundlegende Prinzip hinter der Testing Library gilt auch im Jahr 2026 weiterhin: Tests, die die tatsächliche Nutzung widerspiegeln, verleihen mehr echtes Vertrauen. Wenn eine harmlose Refaktorierung Ihre Tests zum Scheitern bringt, beziehen Sie sich auf die falsche Ebene.

2. Verwendung von fireEvent anstelle von userEvent

fireEvent sendet lediglich rohe DOM-Ereignisse ab. userEvent simuliert hingegen die gesamte Ablaufkette, die eine echte Person ausführt. Es gibt kaum noch einen Grund, in Komponententests auf fireEvent zurückzugreifen – doch dieser Brauch bleibt bestehen, weil es in älteren Anleitungen so gelehrt wurde.

// ❌ Useless: fireEvent does not simulate real interaction
test('toggles the menu', () => {
  render(<Navigation />);
  const button = screen.getByText('Menu');

  fireEvent.click(button); // Just fires a click event

  expect(screen.getByText('Dashboard')).toBeInTheDocument();
});

Das Problem ist, dass fireEvent.click niemals bestätigt, ob das Element tatsächlich von einem Menschen geklickt werden kann. Es könnte deaktiviert sein, von einem anderen Element verdeckt oder überhaupt nicht per Tastatur erreichbar sein. Die Prüfung ist zwar erfolgreich, doch die eigentliche Funktion bleibt unbrauchbar.

// ✅ Useful: userEvent validates the interaction
test('toggles the menu with keyboard and mouse', async () => {
  render(<Navigation />);
  const user = userEvent.setup();
  const button = screen.getByRole('button', { name: /menu/i });

  // Verifies element is focusable and clickable
  await user.click(button);
  expect(screen.getByRole('link', { name: /dashboard/i })).toBeVisible();

  // Tests keyboard accessibility
  await user.keyboard('{Escape}');
  expect(screen.queryByRole('link', { name: /dashboard/i })).not.toBeInTheDocument();
});

userEvent überprüft zunächst, ob das Ziel nicht deaktiviert oder nur zum Lesen zugänglich ist und tatsächlich sichtbar ist, bevor etwas ausgelöst wird, danach werden die realistischen Abfolgen von Zeiger-, Tastatur- und Fokusereignissen abgespielt. Barrierefreiheit ist nicht mehr nur ein zusätzlicher Vorteil, und Ihre Tests sollten dies automatisch berücksichtigen.

3. Falsches Mocking von Netzwerkaufrufen auf der falschen Ebene

Nehmen wir an, Sie testen eine Komponente, die Daten von einer API abruft. Die schnelle Lösung besteht darin, fetch oder axios.get direkt im Testfile zu mocken. Der Test läuft erfolgreich. Danach ändert das Backend-Team einen Feldnamen von userName auf username, die Produktion funktioniert nicht mehr, und Ihre Testsuite bemerkt nichts davon.

// ❌ Useless: Mocking fetch directly
test('displays user profile', async () => {
  global.fetch = vi.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ userName: 'Alice' }), // Drifts from reality
    })
  );

  render(<UserProfile userId="123" />);

  expect(await screen.findByText('Alice')).toBeInTheDocument();
});

Das Problem hier ist, dass Sie gegen eine erfundene API validieren und nicht gegen die echte. Der Stub überprüft weder Header noch die Struktur der URL und simuliert weder Verzögerungen noch Fehlerfälle. Er vermittelt Ihnen ein falsches Gefühl der Sicherheit, während die eigentliche Integration stillschweigend aus dem Takt gerät.

// ✅ Useful: Mocking at the network layer with MSW
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({ username: 'Alice', id: params.id });
  })
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('displays user profile from API', async () => {
  render(<UserProfile userId="123" />);

  // Tests the actual component + network layer together
  expect(await screen.findByText(/alice/i)).toBeInTheDocument();
  expect(screen.getByText(/id: 123/i)).toBeInTheDocument();
});

MSW greift an der Netzwerkschranke ein, sodass Ihre Komponente genau denselben fetch-Aufrufpfad verwendet wie in der Produktion. Wenn sich das API-Vertragswerk ändert, korrigieren Sie den MSW-Handler an einer Stelle, und jeder Test, der auf diesen Endpunkt zugreift, spiegelt automatisch die neue Realität wider. Bis 2026 ist MSW 2.x zur Standardwahl geworden, und das Direkt-Stubbing von fetch stellt im Grunde angesammelten technischen Schulden dar.

4. Verlass auf Snapshots als Ersatz für Regressionstests

Snapshot-Tests sind verlockend, gerade weil sie so wenig von einem verlangen. Man rendernt etwas, speichert die entstehende HTML-Datei ab und überprüft sie. Monate später passt ein Designer ein CSS-Modul an – der Snapshot funktioniert nicht mehr – und jemand genehmigt den neuen Snapshot mit nur einer Tastenbetätigung, ohne jemals wirklich zu prüfen, was sich geändert hat.

// ❌ Useless: Snapshot as a crutch
test('renders correctly', () => {
  const { container } = render(<Card title="Hello" />);
  expect(container.firstChild).toMatchSnapshot();
});

Das Problem ist, dass niemand tatsächlich einen 400 Zeilen langen Diff serialisierter Markup-Dateien durchsieht. Ein Snapshot signalisiert zwar, dass *etwas* sich geändert hat, doch er kann nicht angeben, ob es sich dabei um eine harmlose Designanpassung oder um einen fehlerhaften Layout handelt. Es handelt sich dabei um Lärm, der als Sicherheitsnetz getarnt wird.

// ✅ Useful: Targeted assertions
test('renders card with accessible title and action', () => {
  render(<Card title="Hello" onAction={() => {}} />);

  expect(screen.getByRole('heading', { name: /hello/i })).toBeInTheDocument();
  expect(screen.getByRole('button', { name: /action/i })).toBeEnabled();
  expect(screen.getByRole('article')).toHaveAttribute('aria-labelledby');
});

Durch das Schreiben gezielter Assertionen wird genau beschrieben, welches Verhalten die Komponente bietet. Wenn sich die Markup-Struktur ändert, solange der zugrunde liegende Zugänglichkeitsvertrag weiterhin gilt, besteht der Test weiterhin. Fällt versehentlich eine Überschriftsfunktion weg, erhält man eine präzise Fehlermeldung, die auf das eigentliche Problem hinweist. Mit Blick auf 2026 lohnt es sich, die Snapshot-Dateien vollständig zu löschen und sie durch Assertionen zu ersetzen, die Ihre tatsächlichen Erwartungen darlegen.

5. Elemente ausschließlich über data-testid auswählen

Das Hinzufügen von data-testid zu jedem Element erscheint als sichere, fehlerfreie Option – die Abfrage findet immer ihr Ziel. Doch ein data-testid-Selektor sagt nichts über Zugänglichkeit, Semantik oder die Art und Weise aus, wie ein echter Benutzer die Schnittstelle nutzt. Er bestätigt lediglich, dass ein Entwickler daran gedacht hat, einen bestimmten String einzutippen.

// ❌ Useless: Brittle, tests nothing about accessibility
function Form() {
  return (
    <div data-testid="form-container">
      <input data-testid="email-input" />
      <button data-testid="submit-btn">Submit</button>
    </div>
  );
}
test('submits form', async () => {
  render(<Form />);
  await userEvent.type(screen.getByTestId('email-input'), 'test@example.com');
  await userEvent.click(screen.getByTestId('submit-btn'));
});

Diese Art von Test besteht glücklicherweise auch dann, wenn der Eingabefeld fehlende Beschriftungen aufweist, der „Button“ tatsächlich nur ein gestylter div ist und ein Nutzer mit Screen Reader völlig orientierungslos wäre. Im Grunde zertifiziert dieser Test fehlerhaften Markup als korrekt.

// ✅ Useful: Semantic queries enforce accessibility
function Form() {
  return (
    <form aria-label="Login form">
      <label htmlFor="email">Email address</label>
      <input id="email" type="email" />
      <button type="submit">Submit</button>
    </form>
  );
}
test('submits login form', async () => {
  render(<Form />);
  const user = userEvent.setup();

  // These queries fail if the elements are not accessible
  await user.type(screen.getByLabelText(/email address/i), 'test@example.com');
  await user.click(screen.getByRole('button', { name: /submit/i }));

  expect(screen.getByRole('form', { name: /login form/i })).toBeInTheDocument();
});

Die vom Testing Library empfohlene Reihenfolge der Abfragen ist nicht willkürlich: getByRole, getByLabelText und getByPlaceholderText leiten dazu an, Markup zu verwenden, das tatsächlich barrierefrei ist. Wenn der einzige Weg, ein Element zu erfassen, über einen data-testid erfolgt, ist das in der Regel ein Zeichen dafür, dass die Komponente selbst nicht barrierefrei ist. Angesichts des zunehmenden Einsatzes von KI-gestützten Rechtsstreitigkeiten im Bereich Barrierefreiheit ab 2026 ist das nicht mehr nur ein Test-Shortcut, sondern birgt echte rechtliche Risiken.

6. Überspringen des Wartens auf asynchrone Abläufe

Fast alles in React geschieht mit Verzögerung. Zustandsänderungen werden gebündelt, Effects werden nachdem der Browser das Bild angezeigt hat ausgeführt, und das Abrufen von Daten wird erst bei einem späteren Zeitpunkt abgeschlossen. Wenn ein Test nicht richtig darauf wartet, bis diese Vorgänge abgeschlossen sind, laufen seine Prüfungen zu früh ab und der Test besteht, ohne tatsächlich etwas Bedeutendes überprüft zu haben.

// ❌ Useless: Missing await on async operations
test('shows search results', () => {
  render(<Search />);

  userEvent.type(screen.getByRole('searchbox'), 'react');
  userEvent.keyboard('{Enter}');

  // This runs before the fetch resolves
  expect(screen.getByText('React Testing')).toBeInTheDocument();
});

Je nach Caching oder Zeitpunkt kann ein solcher Test aus Zufall bestehen oder unvorhersehbar fehlen. Das ist vermutlich schlimmer als gar keinen Test zu haben, da es ein falsches Gefühl der Sicherheit erweckt.

// ✅ Useful: Explicitly await async states
test('shows search results after typing', async () => {
  render(<Search />);
  const user = userEvent.setup();

  await user.type(screen.getByRole('searchbox'), 'react');
  await user.keyboard('{Enter}');

  // findBy queries wait for the element to appear
  expect(await screen.findByText('React Testing')).toBeInTheDocument();

  // Ensure loading state was shown and removed
  expect(screen.queryByRole('status', { name: /loading/i })).not.toBeInTheDocument();
});

Im Jahr 2026, mit dem use-Hook von React 19 sowie der breiteren Verwendung von Suspense-Grenzen, kommen asynchrone Muster fast überall vor. Jede Interaktion, die eine Zustandsaktualisierung, einen Übergang oder einen Netzwerkaufruf auslöst, muss ordnungsgemäß abgewartet werden. Verwenden Sie findBy-Abfragen, wenn ein Element asynchron angezeigt wird, nutzen Sie waitFor für Bedingungen, die über einfache Präsenzprüfungen hinausgehen, und stellen Sie sicher, dass jeder userEvent-Aufruf abgewartet wird.

7. Testen von Komponenten isoliert von ihrer tatsächlichen Umgebung

Es ist üblich, eine Komponente völlig eigenständig darzustellen – ohne Router, ohne Theme-Provider, ohne Query-Client, ohne Auth-Context – zu beobachten, wie der Test erfolgreich ist, und anschließend zu sehen, wie dieselbe Komponente in der Produktion versagt, weil sie stumm von einem Context abhing, den der Test nie bereitgestellt hat.

// ❌ Useless: Missing app context
test('renders dashboard', () => {
  render(<Dashboard />);
  expect(screen.getByText('Welcome')).toBeInTheDocument();
});

Der Test sagt nicht viel aus, denn Dashboard stützt sich vermutlich auf Hooks wie useQuery, useAuth oder useNavigate. Ohne die entsprechenden Provider funktioniert die Komponente in Wirklichkeit nicht – man testet vielmehr eine imaginäre Version davon, die in der tatsächlichen Anwendung nicht existiert.

// ✅ Useful: Custom render with all providers
import { render as rtlRender } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
function render(ui, { route = '/', ...options } = {}) {
  window.history.pushState({}, 'Test page', route);

  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  const Wrapper = ({ children }) => (
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <AuthProvider>
          {children}
        </AuthProvider>
      </BrowserRouter>
    </QueryClientProvider>
  );

  return rtlRender(ui, { wrapper: Wrapper, ...options });
}
// Now your tests run in an environment that matches production
test('renders dashboard for authenticated user', async () => {
  render(<Dashboard />, { route: '/dashboard' });

  expect(await screen.findByText(/welcome back/i)).toBeInTheDocument();
});

Angesichts der stark kontextabhängigen modernen Architekturen sowie der weit verbreiteten Caching-Schichten wie React Query wird das Erstellen eines gemeinsamen, benutzerdefinierten Render-Helpers ab 2026 nicht mehr nur vorteilhaft sein – es wird zum Rückgrat der gesamten Anwendung. Jeder Test sollte mit derselben Provider-Kombination ausgeführt werden, die auch die Produktivanwendung tatsächlich verwendet.

8. Nur den erfolgreichen Ablauf abdecken

Die Überprüfung, ob ein Formular mit gültigen Eingaben gesendet wird, ob eine Liste angezeigt wird, wenn die API Daten zurückgibt, oder ob ein Button auf Aktivierung reagiert, ist an sich kein Fehler – doch damit bleibt die Abdeckung erheblich unvollständig, und unvollständige Tests versagen genau dann, wenn Fehler auftreten.

// ❌ Useless: Only the sunny day scenario
test('displays products', async () => {
  server.use(
    http.get('/api/products', () => HttpResponse.json([{ id: 1, name: 'Widget' }]))
  );

  render(<ProductList />);
  expect(await screen.findByText('Widget')).toBeInTheDocument();
});

Diese Art von Test sagt nichts über eine 500-Antwort, ein leeres Ergebnismenge, einen abgelaufenen Anfragezeitraum oder einen Benutzer aus, der fehlerhafte Daten einreicht. Ein großer Teil der tatsächlichen Produktionsfehler – oft auf etwa achtzig Prozent geschätzt – versteckt sich genau in solchen Randfällen.

// ✅ Useful: Testing states that actually happen to users
test('displays empty state when no products exist', async () => {
  server.use(http.get('/api/products', () => HttpResponse.json([])));

  render(<ProductList />);
  expect(await screen.findByText(/no products available/i)).toBeInTheDocument();
});
test('displays error state when API fails', async () => {
  server.use(
    http.get('/api/products', () => new HttpResponse(null, { status: 500 }))
  );

  render(<ProductList />);
  expect(await screen.findByRole('alert')).toHaveTextContent(/failed to load/i);
});
test('prevents submission with invalid email', async () => {
  render(<NewsletterForm />);
  const user = userEvent.setup();

  await user.type(screen.getByLabelText(/email/i), 'not-an-email');
  await user.click(screen.getByRole('button', { name: /subscribe/i }));

  expect(screen.getByRole('alert')).toHaveTextContent(/valid email required/i);
  expect(screen.getByRole('button')).toBeDisabled(); // Submit blocked
});

Da Formularaktionen und optimistische UI-Updates in React 19 zum Standard geworden sind, ist es wichtiger denn je zu prüfen, wie sich Ihre Komponenten in Fehlerrichtungen und Rollback-Szenarien verhalten. Benutzer werden unweigerlich auf Fehlerfälle stoßen, und Ihr Testumfang sollte nachweisen, dass diese Fälle tatsächlich behoben werden.

9. Übertragung des Zustands auf verschiedene Testfälle

Gemeinsamer globaler Zustand ist eine häufige Quelle von Problemen in Testsuiteen. Ein Zustandsspeicher kann Werte von einem Test auf den nächsten übertragen. Ein TanStack Query-Cache kann veraltete Einträge weiterhin speichern. Ein in einer Datei emulierter window.location-Wert kann unbemerkt den nächsten Test beeinflussen. Das Ergebnis ist eine Suite, in der die Tests für sich genommen problemlos bestehen, aber zusammen ausgeführt fehlschlagen – oder, was noch verwirrender ist, im vollständigen Ausführungsablauf bestehen, aber isoliert ausgeführt fehlschlagen.

// ❌ Useless: Polluted global state
import { useAuthStore } from './store';
test('logs in user', () => {
  useAuthStore.getState().setUser({ id: 1, name: 'Alice' });
  render(<Profile />);
  expect(screen.getByText('Alice')).toBeInTheDocument();
});
// This test might fail depending on run order
test('shows guest view', () => {
  render(<Profile />);
  // If the previous test ran first, Alice is still logged in
  expect(screen.getByText(/sign in/i)).toBeInTheDocument();
});

Die Folge ist eine instabile, langsame und unzuverlässige Testsuite. Sobald das eintritt, verlieren die Entwickler das Vertrauen in Fehlermeldungen aus dem CI-System, und die Tests werden zu Hintergrundgeräuschen, die niemand untersucht.

// ✅ Useful: Isolated state for every test
import { act } from '@testing-library/react';
beforeEach(() => {
  // Reset Zustand store
  act(() => {
    useAuthStore.setState({ user: null, isAuthenticated: false });
  });

  // Clear React Query cache
  queryClient.clear();

  // Reset MSW handlers
  server.resetHandlers();

  // Reset mocks
  vi.clearAllMocks();
});
test('logs in user', async () => {
  // Test with explicit setup
  act(() => {
    useAuthStore.getState().setUser({ id: 1, name: 'Alice' });
  });

  render(<Profile />);
  expect(screen.getByText('Alice')).toBeInTheDocument();
});
test('shows guest view', () => {
  // Guaranteed clean state
  render(<Profile />);
  expect(screen.getByText(/sign in/i)).toBeInTheDocument();
});

Im Jahr 2026, mit atomaren State-Libraries und gemeinsamen Caches überall, ist es unerlässlich, die Testisolierung als architektonisches Konzept und nicht als nachträgliche Überlegung zu betrachten. Jeder Test muss seinen eigenen sauberen Kontext erstellen, sein Verhalten überprüfen und danach alles wieder aufräumen. Ein guter Prüfstein: Wenn Ihre Testsuite nicht in einer zufälligen Reihenfolge ausgeführt werden kann, ist etwas an Ihrer Isolierungsstrategie fehlerhaft.

10. Aussagen, die niemals fehlschlagen können

Möglicherweise ist der listigste Fehler das Schreiben eines Tests, der scheinbar gültig ist, aber strukturell nicht in der Lage ist, Fehler aufzudecken. Dies tritt typischerweise dann auf, wenn die Aussage das Falsche zum Ziel hat, ein await weggelassen wird oder ein allzu nachsichtiger Vergleich verwendet wird.

// ❌ Useless: Assertion that cannot fail
test('modal opens', async () => {
  render(<Modal trigger={<button>Open</button>} />);

  // getBy throws if not found, but this is not an assertion
  screen.getByRole('dialog');

  // This passes even if the modal is empty or hidden
  expect(screen.getByRole('dialog')).toBeTruthy();
});
// ❌ Useless: Missing await on expect
test('shows notification', async () => {
  render(<Notifier />);
  await userEvent.click(screen.getByRole('button'));

  // Without await, this checks before the notification appears
  expect(screen.findByText(/success/i)).toBeInTheDocument();
});
// ❌ Useless: Testing presence of truthy object
test('form is valid', () => {
  const { container } = render(<Form />);
  // container is always truthy
  expect(container).toBeTruthy();
});

Tests wie dieser sind vermutlich schlimmer als gar keine Tests – sie verbrauchen weiterhin Zeit in den CI-Prozessen und geben dem Team ein falsches Gefühl der Sicherheit, obwohl sie keine echten Regressionen erkennen können, wenn diese auftreten.

// ✅ Useful: Specific, awaited, negative assertions
test('modal opens and traps focus', async () => {
  render(<Modal trigger={<button>Open</button>} />);
  const user = userEvent.setup();

  await user.click(screen.getByRole('button', { name: /open/i }));

  // Assert visibility, not just presence
  const modal = screen.getByRole('dialog', { name: /confirm/i });
  expect(modal).toBeVisible();

  // Assert focus management
  expect(screen.getByRole('button', { name: /close/i })).toHaveFocus();

  // Assert escape key behavior
  await user.keyboard('{Escape}');
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
test('shows success notification after action', async () => {
  render(<Notifier />);
  const user = userEvent.setup();

  await user.click(screen.getByRole('button'));

  // await the findBy, then assert
  const notification = await screen.findByRole('alert');
  expect(notification).toHaveTextContent(/successfully saved/i);
  expect(notification).toHaveAttribute('aria-live', 'polite');
});

Angesichts des kontinuierlichen Fokus der React Testing Library auf die Überprüfung von Benutzeroberflächen sollte das Verlassen auf schwache Abgleichsfunktionen wie toBeTruthy oder toBeDefined bereits im Jahr 2026 als Warnsignal angesehen werden. Bevorzugen Sie Aussagen zu sichtbarem Text, ARIA-Attributen, deaktiviertem Zustand sowie dem tatsächlich angezeigten Inhalt. Ein nützlicher Test: Wenn die Aussage auch bei einem leeren div noch bestehen bleibt, muss sie neu geschrieben werden.

Die Test-Checkliste für 2026

Vor dem Merge eines neuen Tests gehen Sie diese Liste durch:

  • [ ] Überprüft der Test das Verhalten der Benutzeroberfläche und nicht nur die interne Implementierung?
  • [ ] Steuert es die Interaktionen mit userEvent anstelle von fireEvent?
  • [ ] Wird die Netzwerkschicht über MSW emuliert und nicht durch einen einfachen fetch-Stub?
  • [ ] Setzt es bewusste Aussagen voraus anstatt sich auf Snapshots zu verlassen?
  • [ ] Werden die Elemente nach Rolle und Beschriftung gefunden und nicht über data-testid?
  • [ ] Werden alle asynchronen Schritte ordnungsgemäß abgewartet?
  • [ ] Wird es innerhalb derselben Provider-Hierarchie renderiert, die die echte Anwendung verwendet?
  • [ ] Prüft es Fehler-, leere- und Ladezustände, nicht nur den normalen Ablauf?
  • [ ] Wird der globale Zustand zurückgesetzt, damit der Test in beliebiger Reihenfolge ausgeführt werden kann?
  • [ ] Würde die Aussage tatsächlich fehlschlagen, wenn das zugrunde liegende Verhalten ausfällt?
  • Es reicht nicht aus, einfach nur ein grünes Häkchen von der Testsuite zu erhalten – das eigentliche Ziel ist es, diesem Häkchen vertrauen zu können. Wenn Sie diesen zehn Fallstricken ausweichen, werden Ihre Tests von einer Formalität zu dem echten Sicherheitsnetz, das sie sein sollen.