Komponententests sind ein guter Ausgangspunkt, um praktischen Testcode zu demonstrieren. Komponententests sind umfangreicher als einfache Einheitentests und weniger komplex als End-to-End-Tests und dienen der Interaktion mit dem DOM. Die Verwendung von React macht es Webentwicklern philosophischer geworden, Websites oder Webanwendungen als aus Komponenten bestehenden Elementen zu betrachten.
Das Testen einzelner Komponenten, unabhängig davon, wie komplex sie sind, ist also eine gute Möglichkeit, über das Testen einer neuen oder vorhandenen Anwendung nachzudenken.
Auf dieser Seite wird das Testen einer kleinen Komponente mit komplexen externen Abhängigkeiten beschrieben. Es ist einfach, eine Komponente zu testen, die nicht mit anderem Code interagiert. Dazu können Sie beispielsweise auf eine Schaltfläche klicken und bestätigen, dass sich eine Zahl erhöht. In Wirklichkeit ist jedoch nur sehr wenig Code so. Das Testen von Code, der keine Interaktionen hat, kann von begrenztem Wert sein.
Dies ist nicht als vollständige Anleitung gedacht. In einem späteren Abschnitt, „Automatisierte Tests in der Praxis“, wird das Testen einer echten Website mit Beispielcode beschrieben, den Sie als Anleitung verwenden können. Auf dieser Seite finden Sie jedoch immer noch einige Beispiele für praktische Komponententests.
Die zu testende Komponente
Wir verwenden Vitest und dessen JSDOM-Umgebung, um eine React-Komponente zu testen. Auf diese Weise können wir mit Node.js schnell Tests in der Befehlszeile ausführen, während wir einen Browser emulieren.
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 zeigt keine Best Practices für React (z. B. wird fetch
in useEffect
verwendet). Es ist aber wahrscheinlich, dass Ihre Codebasis viele solche Fälle enthält. Genauer gesagt können diese Fälle auf den ersten Blick seltsam erscheinen. In einem späteren Abschnitt dieses Kurses wird das Schreiben von testbarem Code ausführlich behandelt.
Dies sind die Elemente, die wir in diesem Beispiel testen:
- Prüfen Sie, ob als Reaktion auf Daten aus dem Netzwerk ein korrektes DOM erstellt wird.
- Vergewissern Sie sich, dass ein Klick auf einen Nutzer einen Rückruf auslöst.
Jede Komponente ist anders. Warum ist das Testen so interessant?
- Sie verwendet den globalen
fetch
, um reale Daten aus dem Netzwerk anzufordern, die beim Testen instabil oder langsam sein können. - Es importiert eine weitere Klasse,
UserRow
, die wir möglicherweise nicht implizit testen möchten. - Dabei wird ein
Context
verwendet, der nicht Teil des zu testenden Codes ist und normalerweise von einer übergeordneten Komponente bereitgestellt wird.
Schreiben Sie zuerst einen Schnelltest
Wir können schnell etwas sehr Grundlegendes über diese Komponente testen. Zur Klarstellung: Dieses Beispiel
ist nicht sehr nützlich! Es ist jedoch hilfreich, den Textbaustein in einer Peer-Datei namens UserList.test.tsx
einzurichten (denken Sie daran, dass Test-Runner wie Vitest standardmäßig Dateien ausführen, die mit .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 der Komponente den Text „Users“ enthält.
Sie funktioniert, auch wenn die Komponente den Nebeneffekt hat, dass eine fetch
an das Netzwerk gesendet wird. fetch
wird am Ende des Tests noch ausgeführt und es wurde kein Endpunkt festgelegt. Wir können nicht bestätigen, dass Nutzerinformationen nach dem Ende des Tests angezeigt werden, zumindest nicht ohne ein Zeitlimit zu warten.
Mock fetch()
Beim Mocking wird eine echte Funktion oder Klasse im Rahmen eines Tests durch etwas ersetzt, das Sie kontrollieren. Dies ist mit Ausnahme der einfachsten Einheitentests bei fast allen Testtypen üblich. Dies wird unter Assertions und andere Primitive ausführlicher behandelt.
Sie können die fetch()
für Ihren Test simulieren, damit er schnell abgeschlossen wird und die erwarteten Daten zurückgibt, keine Daten vom Typ „real“ oder „unbekannt“. fetch
ist ein global, d. h. wir müssen weder import
noch require
in unseren Code einfügen.
In Vitest können Sie ein globales Modell simulieren, indem Sie vi.stubGlobal
mit einem speziellen Objekt aufrufen, das von vi.fn()
zurückgegeben wird. Dadurch wird eine Simulation erstellt, die später geändert werden kann. Diese Methoden werden in einem späteren Abschnitt dieses Kurses genauer betrachtet. Im folgenden Code können Sie sie in der Praxis sehen:
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 Mock hinzugefügt, eine „gefälschte“ Version des Netzwerkabrufs Response
beschrieben und gewartet, bis sie angezeigt wird. Wenn der Text nicht angezeigt wird – können Sie dies prüfen, indem Sie die Abfrage in queryByText
ändern – schlägt der Test fehl.
In diesem Beispiel wurden die integrierten Mocking-Hilfsmittel von Vitest verwendet. Andere Test-Frameworks haben jedoch einen ähnlichen Ansatz für das Mocking. Vitest ist insofern einmalig, als Sie nach allen Tests vi.unstubAllGlobals()
aufrufen oder eine äquivalente globale Option festlegen müssen. Wenn unsere Arbeit nicht „rückgängig gemacht“ wird, kann sich das fetch
-Mock auf andere Tests auswirken. Jede Anfrage wird mit einem anderen JSON-Datenstapel beantwortet.
Mock-Importe
Vielleicht haben Sie schon bemerkt, dass unsere UserList
-Komponente selbst eine Komponente namens UserRow
importiert. Auch wenn der Code nicht eingefügt wurde, wird der Name des Nutzers gerendert. Bei den vorherigen Testprüfungen wird nach „Sam“ gesucht und der Code wird nicht direkt in UserList
gerendert, daher muss er aus UserRow
stammen.
UserRow
kann jedoch selbst eine komplexe Komponente sein. Sie könnte weitere Nutzerdaten abrufen oder Nebenwirkungen haben, die für unseren Test nicht relevant sind. Durch das Entfernen dieser Variabilität werden Ihre Tests hilfreicher, insbesondere da die Komponenten, die Sie testen möchten, komplexer und verflochtener mit ihren Abhängigkeiten werden.
Glücklicherweise können Sie Vitest verwenden, um bestimmte Importe zu simulieren, selbst wenn sie in Ihrem Test nicht direkt verwendet werden. Daher wird jeder Code, der sie verwendet, mit einer einfachen oder bekannten Version bereitgestellt:
vi.mock('./UserRow.tsx', () => {
return {
UserRow(arg) {
return <>{arg.u.name}</>;
},
}
});
test('render', async () => {
// ...
});
Wie das Simulieren des globalen fetch
ist dies ein leistungsstarkes Tool, aber es kann unrentabel werden, wenn Ihr Code viele Abhängigkeiten hat. Die beste Lösung hierfür ist
das Schreiben von testbarem Code.
Klicken und Kontext angeben
React und andere Bibliotheken wie Lit haben ein Konzept namens Context
. Der Beispielcode enthält ein UserContext
, das die Methode aufruft, wenn ein Nutzer ausgewählt wird. Dies wird oft als Alternative zum "Prop-Drilldown" betrachtet, bei dem der Callback direkt an UserList
übergeben wird.
In dem von uns geschriebenen Test-Harnisch wurden UserContext
nicht bereitgestellt. Wenn Sie dem React-Test eine Klickaktion ohne sie hinzufügen, kommt es im schlimmsten Fall zum Absturz des Tests. Wenn an anderer Stelle eine Standardinstanz bereitgestellt wird, führt dies im schlimmsten Fall zu einem Verhalten, das außerhalb unserer Kontrolle liegt (ähnlich wie bei einem unbekannten UserRow
oben):
const c = render(<UserList />);
const chooseButton = await c.getByText(/Choose);
chooseButton.click();
Stattdessen können Sie beim Rendern der Komponente Ihren eigenen Context
angeben. In diesem Beispiel wird eine Instanz der vi.fn()
, einer Vitest-Mock-Funktion, verwendet, mit der geprüft werden kann, ob ein Aufruf erfolgt ist und mit welchen Argumenten er ausgeführt wurde. In unserem Fall interagiert dies mit dem mockierten fetch
im vorherigen Beispiel und 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 zu testenden Kernkomponente entfernen können.
Zusammenfassung
Dies war ein schnelles und vereinfachtes Beispiel, das zeigt, wie Sie einen Komponententest erstellen, um eine schwierig zu testende React-Komponente zu testen und zu schützen. Wichtig ist dabei, dass die Komponente richtig mit ihren Abhängigkeiten interagiert (globale fetch
, importierte Unterkomponente und Context
).
Wissen testen
Mit welchen Ansätzen wurde die React-Komponente getestet?