Erhöhen Sie die Wartbarkeit von Code mit React-Integrationstests

Veröffentlicht: 2022-03-11

Integrationstests sind ein idealer Punkt zwischen Kosten und Wert von Tests. Das Schreiben von Integrationstests für eine React-App mit Hilfe der React-Testing-Bibliothek anstelle von oder zusätzlich zu Komponententests kann die Code-Wartbarkeit erhöhen, ohne die Entwicklungsgeschwindigkeit zu beeinträchtigen.

Falls Sie sich einen Vorsprung verschaffen möchten, bevor wir fortfahren, können Sie hier ein Beispiel für die Verwendung der React-Testing-Bibliothek für React-App-Integrationstests sehen.

Warum in Integrationstests investieren?

"Integrationstests bieten ein hervorragendes Gleichgewicht zwischen Vertrauen und Geschwindigkeit/Kosten. Deshalb ist es ratsam, den größten Teil (nicht alle, wohlgemerkt) Ihrer Bemühungen darauf zu verwenden."
– Kent C. Dodds in Schreibtests. Nicht zu viele. Meist Integration.

Es ist üblich, Unit-Tests für React-Komponenten zu schreiben, wobei häufig eine beliebte Bibliothek zum Testen von React-„Enzymen“ verwendet wird; insbesondere seine „flache“ Methode. Dieser Ansatz ermöglicht es uns, Komponenten isoliert vom Rest der App zu testen. Da es beim Schreiben von React-Apps jedoch ausschließlich um das Zusammenstellen von Komponenten geht, stellen Unit-Tests allein nicht sicher, dass die App fehlerfrei ist.

Beispielsweise kann das Ändern der akzeptierten Props einer Komponente und das Aktualisieren der zugehörigen Komponententests dazu führen, dass alle Tests bestanden werden, während die App möglicherweise immer noch fehlerhaft ist, wenn eine andere Komponente nicht entsprechend aktualisiert wurde.

Integrationstests können dabei helfen, bei Änderungen an einer React-App beruhigt zu bleiben, da sie sicherstellen, dass die Zusammensetzung der Komponenten zur gewünschten UX führt.

Anforderungen für React-App-Integrationstests

Hier sind einige der Dinge, die React-Entwickler beim Schreiben von Integrationstests tun möchten :

  • Testen Sie Anwendungsfälle aus der Benutzerperspektive. Benutzer greifen auf Informationen auf einer Webseite zu und interagieren mit verfügbaren Steuerelementen.
  • Mock-API-Aufrufe, um nicht von der API-Verfügbarkeit und dem Status für das Bestehen/Nichtbestehen von Tests abhängig zu sein.
  • Schein-Browser-APIs (z. B. lokaler Speicher), da sie in der Testumgebung einfach nicht vorhanden sind.
  • Assert on React DOM state (Browser-DOM oder eine native mobile Umgebung).

Nun, für einige Dinge, die wir beim Schreiben von React-App-Integrationstests vermeiden sollten:

  • Details der Testimplementierung. Implementierungsänderungen sollten einen Test nur brechen, wenn sie tatsächlich einen Fehler eingeführt haben.
  • Spott zu viel. Wir wollen testen, wie alle Teile der App zusammenarbeiten.
  • Flach rendern. Wir wollen die Zusammensetzung aller Komponenten in der App bis auf die kleinste Komponente testen.

Warum React-testing-library wählen?

Die oben genannten Anforderungen machen die React-Testing-Library zu einer guten Wahl, da ihr wichtigstes Leitprinzip darin besteht, dass React-Komponenten so getestet werden können, dass sie der Verwendung durch einen tatsächlichen Menschen ähneln.

Die Bibliothek ermöglicht es uns zusammen mit ihren optionalen Begleitbibliotheken, Tests zu schreiben, die mit DOM interagieren und seinen Zustand bestätigen.

Beispiel-App-Setup

Die App, für die wir Beispiel-Integrationstests schreiben werden, implementiert ein einfaches Szenario:

  • Der Benutzer gibt einen GitHub-Benutzernamen ein.
  • Die App zeigt eine Liste öffentlicher Repositories an, die dem eingegebenen Benutzernamen zugeordnet sind.

Wie die obige Funktionalität implementiert wird, sollte aus Sicht des Integrationstests irrelevant sein. Um jedoch nahe an realen Anwendungen zu bleiben, folgt die App gängigen React-Mustern, daher die App:

  • Ist eine Single-Page-App (SPA).
  • Macht API-Anfragen.
  • Hat globale Zustandsverwaltung.
  • Unterstützt die Internationalisierung.
  • Verwendet eine React-Komponentenbibliothek.

Den Quellcode für die App-Implementierung finden Sie hier.

Integrationstests schreiben

Abhängigkeiten installieren

Mit Garn:

 yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock

Oder mit npm:

 npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock

Erstellen einer Integrationstest-Suite-Datei

Wir erstellen eine Datei mit dem Namen viewGitHubRepositoriesByUsername.spec.js im Ordner ./test unserer Anwendung. Jest wird es automatisch abholen.

Abhängigkeiten in die Testdatei importieren

 import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test

Einrichten der Test-Suite

 describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });

Anmerkungen:

  • Mocken Sie vor allen Tests die GitHub-API nach, um eine Liste von Repositories zurückzugeben, wenn sie mit einem bestimmten Benutzernamen aufgerufen wird.
  • Reinigen Sie nach jedem Test das Test React DOM, sodass jeder Test an einer sauberen Stelle beginnt.
  • describe spezifizieren den Anwendungsfall des Integrationstests und die Flussvariationen.
  • Die Flussvariationen, die wir testen, sind:
    • Der Benutzer gibt einen gültigen Benutzernamen ein, dem öffentliche GitHub-Repositories zugeordnet sind.
    • Der Benutzer gibt einen gültigen Benutzernamen ein, dem keine öffentlichen GitHub-Repositories zugeordnet sind.
    • Der Benutzer gibt einen Benutzernamen ein, der auf GitHub nicht existiert.
  • it blockiert die Verwendung von asynchronem Callback, da der Anwendungsfall, den sie testen, einen asynchronen Schritt enthält.

Schreiben des ersten Flow-Tests

Zuerst muss die App gerendert werden.

 const { getByText, getByPlaceholderText, queryByText } = render(<App />);

Die aus dem @testing-library/react Modul importierte render -Methode rendert die App im Test-React-DOM und gibt DOM-Abfragen zurück, die an den gerenderten App-Container gebunden sind. Diese Abfragen werden verwendet, um DOM-Elemente zu lokalisieren, mit denen interagiert und die bestätigt werden sollen.

Als erster Schritt des zu testenden Ablaufs wird dem Benutzer nun ein Benutzernamensfeld präsentiert, in das er eine Zeichenfolge für den Benutzernamen eingibt.

 userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);

Das userEvent -Hilfsprogramm aus dem importierten @testing-library/user-event Modul verfügt über eine type , die das Verhalten des Benutzers imitiert, wenn er Text in ein Textfeld eingibt. Es akzeptiert zwei Parameter: das DOM-Element, das die Eingabe akzeptiert, und die Zeichenfolge, die der Benutzer eingibt.

Benutzer finden DOM-Elemente normalerweise anhand des ihnen zugeordneten Textes. Bei der Eingabe handelt es sich entweder um Beschriftungstext oder um Platzhaltertext. Die zuvor von render zurückgegebene getByPlaceholderText Abfragemethode ermöglicht es uns, das DOM-Element anhand des Platzhaltertexts zu finden.

Bitte beachten Sie, dass es am besten ist, sich nicht auf die tatsächlichen Lokalisierungswerte zu verlassen und stattdessen das Lokalisierungsmodul so zu konfigurieren, dass es einen Lokalisierungselementschlüssel als seinen Wert zurückgibt, da sich der Text selbst häufig ändern wird.

Wenn beispielsweise die Lokalisierung „en-US“ normalerweise Enter GitHub username als Wert für den Schlüssel userSelection.usernamePlaceholder zurückgeben würde, möchten wir, dass in Tests userSelection.usernamePlaceholder zurückgegeben wird.

Wenn der Benutzer Text in ein Feld eingibt, sollte der Textfeldwert aktualisiert angezeigt werden.

 expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);

Als Nächstes klickt der Benutzer im Ablauf auf die Schaltfläche „Senden“ und erwartet, die Liste der Repositories zu sehen.

 userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');

Die userEvent.click Methode imitiert den Benutzer, der auf ein DOM-Element klickt, während die getByText Abfrage ein DOM-Element anhand des darin enthaltenen Textes findet. Der closest Modifikator stellt sicher, dass wir das Element der richtigen Art auswählen.

Hinweis: In Integrationstests dienen Schritte häufig sowohl der act als auch der assert . Beispielsweise behaupten wir, dass der Benutzer auf eine Schaltfläche klicken kann, indem er darauf klickt.

Im vorherigen Schritt haben wir behauptet, dass der Benutzer den Abschnitt mit der Repository-Liste der App sieht. Nun müssen wir behaupten, dass der Benutzer einen Hinweis darauf sieht, dass der Abruf im Gange ist, da das Abrufen der Liste der Repositories von GitHub einige Zeit dauern kann. Wir möchten auch sicherstellen, dass die App dem Benutzer nicht mitteilt, dass dem eingegebenen Benutzernamen keine Repositories zugeordnet sind, während die Repositories-Liste noch abgerufen wird.

 getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();

Beachten Sie, dass das getBy Abfragepräfix verwendet wird, um zu behaupten, dass das DOM-Element gefunden werden kann, und das queryBy Abfragepräfix für die entgegengesetzte Behauptung nützlich ist. Außerdem gibt queryBy keinen Fehler zurück, wenn kein Element gefunden wird.

Als Nächstes möchten wir sicherstellen, dass die App das Abrufen von Repositories schließlich abschließt und sie dem Benutzer anzeigt.

 await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));

Die asynchrone Methode waitForElement wird verwendet, um auf eine DOM-Aktualisierung zu warten, die die als Methodenparameter bereitgestellte Assertion wahr macht. In diesem Fall behaupten wir, dass die App den Namen und die Beschreibung für jedes Repository anzeigt, das von der verspotteten GitHub-API zurückgegeben wird.

Schließlich sollte die App keinen Indikator mehr anzeigen, dass Repositories abgerufen werden, und es sollte keine Fehlermeldung mehr anzeigen.

 expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();

Unser resultierender React-Integrationstest sieht folgendermaßen aus:

 it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });

Alternative Durchflusstests

Wenn der Benutzer einen GitHub-Benutzernamen ohne zugeordnete öffentliche Repositories eingibt, zeigt die App eine entsprechende Meldung an.

 describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });

Wenn der Benutzer einen GitHub-Benutzernamen eingibt, der nicht existiert, zeigt die App eine Fehlermeldung an.

 describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });

Warum React Integrationstests rocken

Integrationstests bieten wirklich einen idealen Punkt für React-Anwendungen. Diese Tests helfen, Fehler zu finden und den TDD-Ansatz zu verwenden, während sie gleichzeitig keine Wartung erfordern, wenn sich die Implementierung ändert.

Die React-Testing-Bibliothek, die in diesem Artikel vorgestellt wird, ist ein großartiges Tool zum Schreiben von React-Integrationstests, da Sie mit der App wie der Benutzer interagieren und den Status und das Verhalten der App aus der Perspektive des Benutzers validieren können.

Hoffentlich helfen Ihnen die hier bereitgestellten Beispiele beim Schreiben von Integrationstests für neue und bestehende React-Projekte. Den vollständigen Beispielcode, der die App-Implementierung enthält, finden Sie auf meinem GitHub.