Komponententests in der Praxis

Komponententests sind ein guter Ausgangspunkt, um praktischen Testcode zu demonstrieren. Komponententests sind umfangreicher als einfache Unit-Tests, weniger komplex als End-to-End-Tests und zeigen die Interaktion mit dem DOM. In philosophischer Hinsicht hat die Verwendung von React es Webentwicklern erleichtert, Websites oder Webanwendungen als aus Komponenten bestehende Systeme zu betrachten.

Daher ist das Testen einzelner Komponenten, unabhängig von ihrer Komplexität, ein guter Ausgangspunkt, um über das Testen einer neuen oder vorhandenen Anwendung nachzudenken.

Auf dieser Seite wird der Test einer kleinen Komponente mit komplexen externen Abhängigkeiten veranschaulicht. Es ist einfach, eine Komponente zu testen, die nicht mit anderen Code interagiert, z. B. durch Klicken auf eine Schaltfläche und Prüfen, ob eine Zahl steigt. In der Realität ist das nur selten der Fall. Der Test von Code ohne Interaktionen kann daher nur begrenzt sinnvoll sein.

Die zu testende Komponente

Wir verwenden Vitest und die JSDOM-Umgebung, um eine React-Komponente zu testen. So können wir schnell Tests mit Node.js in der Befehlszeile ausführen und dabei einen Browser emulieren.

Eine Liste mit Namen, neben denen jeweils die Schaltfläche „Auswählen“ angezeigt wird.
Eine kleine React-Komponente, die eine Liste der Nutzer des Netzwerks enthält.

Die React-Komponente namens UserList ruft eine Liste von Nutzern aus dem Netzwerk ab und lässt Sie einen davon auswählen. Die Liste der Nutzer wird mit fetch in einem useEffect abgerufen und der Auswahl-Handler wird von Context übergeben. Das ist der Code:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

Dieses Beispiel veranschaulicht nicht die Best Practices für React (z. B. wird fetch in useEffect verwendet), aber Ihre Codebasis enthält wahrscheinlich viele ähnliche Fälle. Genauer gesagt, können diese Fälle auf den ersten Blick schwierig zu testen erscheinen. In einem späteren Abschnitt dieses Kurses geht es im Detail um das Schreiben von testbarem Code.

In diesem Beispiel testen wir Folgendes:

  • Prüfen Sie, ob ein korrektes DOM als Reaktion auf Daten aus dem Netzwerk erstellt wird.
  • Prüfen Sie, ob ein Klick auf einen Nutzer einen Rückruf auslöst.

Jede Komponente ist anders. Was macht das Testen so interessant?

  • Dabei wird die globale fetch verwendet, um Echtzeitdaten vom Netzwerk anzufordern, die während des Tests möglicherweise unzuverlässig oder langsam sind.
  • Es wird eine weitere Klasse importiert, UserRow, die wir möglicherweise nicht implizit testen möchten.
  • Es verwendet ein Context, das nicht speziell Teil des zu testenden Codes ist und normalerweise von einer übergeordneten Komponente bereitgestellt wird.

Erstelle einen kurzen Test, um zu beginnen

Wir können schnell etwas ganz Einfaches an dieser Komponente testen. Dieses Beispiel ist nicht sehr hilfreich. Es ist jedoch hilfreich, die Vorlage in einer Peerdatei namens UserList.test.tsx einzurichten. Denken Sie daran, dass Testläufer wie Vitest standardmäßig Dateien ausführen, die auf .test.js oder ähnlich enden, einschließlich .tsx:

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

Dieser Test bestätigt, dass die Komponente beim Rendern den Text „Users“ enthält. Die Funktionsweise ist trotz der Nebenwirkung der Komponente, dass eine fetch an das Netzwerk gesendet wird, nicht beeinträchtigt. Der Test fetch ist am Ende des Tests noch nicht abgeschlossen und es wurde kein Endpunkt festgelegt. Wir können nicht bestätigen, dass nach Abschluss des Tests Nutzerinformationen angezeigt werden, zumindest nicht ohne eine Zeitüberschreitung.

Mockup fetch()

Beim Mocking wird eine echte Funktion oder Klasse für einen Test durch etwas ersetzt, das Sie kontrollieren können. Dies ist bei fast allen Arten von Tests üblich, mit Ausnahme der einfachsten Einheitentests. Weitere Informationen finden Sie unter Behauptungen und andere Primitive.

Sie können fetch() für Ihren Test simulieren, damit er schnell abgeschlossen wird und die erwarteten Daten zurückgibt, keine „echten“ oder unbekannten Daten. fetch ist global. Das bedeutet, dass wir import oder require nicht in unseren Code einfügen müssen.

In vitest können Sie ein globales Objekt mocken, indem Sie vi.stubGlobal mit einem speziellen Objekt aufrufen, das von vi.fn() zurückgegeben wird. Dadurch wird ein Mock erstellt, das wir später ändern können. Diese Methoden werden in einem späteren Abschnitt dieses Kurses genauer untersucht. Im folgenden Code sehen Sie sie jedoch in der Praxis:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

Mit diesem Code wird ein Mockup hinzugefügt, eine gefälschte Version des Netzwerkabrufs Response beschrieben und dann darauf gewartet, dass er angezeigt wird. Wenn der Text nicht angezeigt wird, was Sie prüfen können, indem Sie die Abfrage in queryByText in einen neuen Namen ändern, schlägt der Test fehl.

In diesem Beispiel wurden die integrierten Mocking-Hilfsfunktionen von Vitest verwendet. Andere Testframeworks haben jedoch ähnliche Ansätze für das Mocking. Vitest ist insofern einzigartig, als Sie vi.unstubAllGlobals() nach allen Tests aufrufen oder eine entsprechende globale Option festlegen müssen. Wenn wir unsere Arbeit nicht rückgängig machen, kann sich der fetch-Mock auf andere Tests auswirken und jede Anfrage wird mit unserem merkwürdigen JSON-Stapel beantwortet.

Mock-Importe

Sie haben vielleicht bemerkt, dass unsere UserList-Komponente selbst eine Komponente namens UserRow importiert. Wir haben den Code nicht eingefügt, aber Sie sehen, dass der Name des Nutzers gerendert wird: Im vorherigen Test wird nach „Sam“ gesucht. Da dieser Name nicht direkt in UserList gerendert wird, muss er aus UserRow stammen.

Ein Flussdiagramm, das zeigt, wie die Namen der Nutzer durch unsere Komponente geleitet werden.
UserListTest kann UserRow nicht sehen.

UserRow kann jedoch selbst eine komplexe Komponente sein. Es können weitere Nutzerdaten abgerufen oder Nebenwirkungen auftreten, die für unseren Test nicht relevant sind. Wenn Sie diese Variabilität entfernen, sind Ihre Tests hilfreicher, insbesondere wenn die zu testenden Komponenten komplexer werden und stärker mit ihren Abhängigkeiten verwoben sind.

Glücklicherweise können Sie mit Vitest bestimmte Importe simulieren, auch wenn sie in Ihrem Test nicht direkt verwendet werden. So wird jeder Code, in dem sie verwendet werden, mit einer einfachen oder bekannten Version bereitgestellt:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

Wie das Mocking der globalen fetch ist dies ein leistungsstarkes Tool, das aber unzuverlässig werden kann, wenn Ihr Code viele Abhängigkeiten hat. Auch hier besteht die beste Lösung darin, testbaren Code zu schreiben.

Klicken und Kontext angeben

React und andere Bibliotheken wie Lit haben ein Konzept namens Context. Der Beispielcode enthält UserContext, die Methode wird aufgerufen, wenn ein Nutzer ausgewählt wird. Dies wird oft als Alternative zum „Prop-Drilling“ angesehen, bei dem der Rückruf direkt an UserList übergeben wird.

UserContext wurde in unserem Test-Harness nicht bereitgestellt. Wenn Sie dem React-Test eine Klickaktion hinzufügen, ohne dass diese vorhanden ist, kann dies im schlimmsten Fall zum Absturz des Tests führen. Wenn eine Standardinstanz an anderer Stelle angegeben wurde, kann dies zu unerwartetem Verhalten führen (ähnlich wie bei einer unbekannten UserRow oben).

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

Stattdessen können Sie beim Rendern der Komponente Ihre eigene Context angeben. In diesem Beispiel wird eine Instanz von vi.fn() verwendet, einer Vitest-Mock-Funktion, mit der überprüft werden kann, ob ein Aufruf erfolgt ist und welche Argumente verwendet wurden.

In unserem Fall interagiert dies mit der gemockten fetch aus dem vorherigen Beispiel. Der Test kann bestätigen, dass die übergebene ID sam war:

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

Dies ist ein einfaches, aber leistungsstarkes Muster, mit dem Sie irrelevante Abhängigkeiten von der Kernkomponente entfernen können, die Sie testen möchten.

Zusammenfassung

In diesem Beispiel wurde gezeigt, wie ein Komponententest erstellt wird, um eine schwer zu testende React-Komponente zu testen und zu schützen. Bei diesem Test ging es darum, sicherzustellen, dass die Komponente korrekt mit ihren Abhängigkeiten interagiert: der globalen fetch, einer importierten Unterkomponente und einer Context.

Wissenstest

Welche Ansätze wurden zum Testen der React-Komponente verwendet?

Komplexe Abhängigkeiten für Tests durch einfache Mocks ersetzen
Globale Variablen als Stubs definieren
Prüfen, ob eine Zahl inkrementiert wurde
Abhängigkeitsinjektion mit Context