Testgetriebene React.js-Entwicklung: React.js Unit Testing mit Enzyme und Jest
Veröffentlicht: 2022-03-11Jeder Code, der keine Tests hat, wird laut Michael Feathers als Legacy-Code bezeichnet. Daher ist eine der besten Möglichkeiten, die Erstellung von Legacy-Code zu vermeiden, die Verwendung von testgetriebener Entwicklung (TDD).
Obwohl viele Tools für JavaScript- und React.js-Einheitentests verfügbar sind, werden wir in diesem Beitrag Jest und Enzyme verwenden, um eine React.js-Komponente mit grundlegenden Funktionen unter Verwendung von TDD zu erstellen.
Warum TDD verwenden, um eine React.js-Komponente zu erstellen?
TDD bringt viele Vorteile für Ihren Code – einer der Vorteile einer hohen Testabdeckung besteht darin, dass es ein einfaches Code-Refactoring ermöglicht und gleichzeitig Ihren Code sauber und funktionsfähig hält.
Wenn Sie schon einmal eine React.js-Komponente erstellt haben, haben Sie festgestellt, dass Code sehr schnell wachsen kann. Es füllt sich mit vielen komplexen Bedingungen, die durch Aussagen zu Zustandsänderungen und Servicerufen verursacht werden.
Jede Komponente ohne Unit-Tests hat Legacy-Code, der schwierig zu warten ist. Wir könnten Einheitentests hinzufügen, nachdem wir den Produktionscode erstellt haben. Wir laufen jedoch Gefahr, einige Szenarien zu übersehen, die hätten getestet werden sollen. Indem wir zuerst Tests erstellen, haben wir eine höhere Chance, jedes Logikszenario in unserer Komponente abzudecken, was das Refactoring und die Wartung vereinfachen würde.
Wie testen wir eine Komponente von React.js?
Es gibt viele Strategien, mit denen wir eine React.js-Komponente testen können:
- Wir können überprüfen, ob eine bestimmte Funktion in
propsaufgerufen wurde, wenn ein bestimmtes Ereignis ausgelöst wurde. - Wir können auch das Ergebnis der
rendererhalten, wenn der Zustand der aktuellen Komponente gegeben ist, und es mit einem vordefinierten Layout abgleichen. - Wir können sogar prüfen, ob die Anzahl der Kinder der Komponente mit einer erwarteten Menge übereinstimmt.
Um diese Strategien anzuwenden, werden wir zwei Tools verwenden, die sich für die Arbeit mit Tests in React.js als nützlich erweisen: Jest und Enzyme.
Verwenden von Jest zum Erstellen von Komponententests
Jest ist ein von Facebook erstelltes Open-Source-Testframework, das sich hervorragend in React.js integrieren lässt. Es enthält ein Befehlszeilentool für die Testausführung, ähnlich dem, was Jasmine und Mocha anbieten. Es erlaubt uns auch, Mock-Funktionen mit fast null Konfiguration zu erstellen und bietet eine wirklich schöne Reihe von Matchern, die das Lesen von Behauptungen erleichtern.
Darüber hinaus bietet es eine wirklich nette Funktion namens „Snapshot-Testing“, die uns hilft, das Ergebnis des Komponenten-Renderings zu überprüfen und zu verifizieren. Wir verwenden Snapshot-Tests, um den Baum einer Komponente zu erfassen und in einer Datei zu speichern, die wir verwenden können, um ihn mit einem Rendering-Baum zu vergleichen (oder was auch immer wir als erstes Argument an die expect übergeben).
Verwenden von Enzym zum Mounten von React.js-Komponenten
Enzyme bietet einen Mechanismus zum Mounten und Durchlaufen von React.js-Komponentenbäumen. Dies hilft uns, Zugriff auf seine eigenen Eigenschaften und seinen Zustand sowie auf seine untergeordneten Requisiten zu erhalten, um unsere Behauptungen auszuführen.
Enzyme bietet zwei Grundfunktionen für die Komponentenmontage: shallow und mount . Die shallow Funktion lädt nur die Root-Komponente in den Speicher, während mount den vollständigen DOM-Baum lädt.
Wir werden Enzyme und Jest kombinieren, um eine React.js-Komponente zu mounten und Behauptungen darüber laufen zu lassen.
Einrichten unserer Umgebung
Sie können sich dieses Repo ansehen, das die grundlegende Konfiguration zum Ausführen dieses Beispiels enthält.
Wir verwenden die folgenden Versionen:
{ "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }Erstellen der React.js-Komponente mit TDD
Der erste Schritt besteht darin, einen Fehlertest zu erstellen, der versucht, eine React.js-Komponente mithilfe der flachen Funktion des Enzyms zu rendern.
// MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });Nach dem Ausführen des Tests erhalten wir die folgende Fehlermeldung:
ReferenceError: MyComponent is not defined.Anschließend erstellen wir die Komponente, die die grundlegende Syntax bereitstellt, um den Test zu bestehen.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } } Im nächsten Schritt stellen wir mithilfe toMatchSnapshot Funktion von Jest sicher, dass unsere Komponente ein vordefiniertes UI-Layout rendert.
Nach dem Aufrufen dieser Methode erstellt Jest automatisch eine Snapshot-Datei mit dem Namen [testFileName].snap , die dem Ordner __snapshots__ hinzugefügt wird.
Diese Datei stellt das UI-Layout dar, das wir von unserem Komponenten-Rendering erwarten.
Da wir jedoch versuchen, reines TDD durchzuführen, sollten wir diese Datei zuerst erstellen und dann die toMatchSnapshot Funktion aufrufen, damit der Test fehlschlägt.
Dies mag etwas verwirrend klingen, da wir nicht wissen, welches Format Jest verwendet, um dieses Layout darzustellen.
Sie könnten versucht sein, zuerst die toMatchSnapshot Funktion auszuführen und das Ergebnis in der Snapshot-Datei anzuzeigen, und das ist eine gültige Option. Wenn wir jedoch wirklich reines TDD verwenden wollen, müssen wir lernen, wie Snapshot-Dateien aufgebaut sind.
Die Snapshot-Datei enthält ein Layout, das dem Namen des Tests entspricht. Das bedeutet, wenn unser Test diese Form hat:
desc("ComponentA" () => { it("should do something", () => { … } }); Wir sollten dies im Exportbereich angeben: Component A should do something 1 .
Hier können Sie mehr über Snapshot-Tests lesen.
Also erstellen wir zuerst die Datei MyComponent.test.js.snap .
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;Dann erstellen wir den Komponententest, der überprüft, ob der Snapshot mit den untergeordneten Elementen der Komponente übereinstimmt.

// MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ... Wir können components.getElements als Ergebnis der render-Methode betrachten.
Wir übergeben diese Elemente an die expect -Methode, um die Überprüfung anhand der Snapshot-Datei durchzuführen.
Nach dem Ausführen des Tests erhalten wir folgenden Fehler:
Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array [] Jest teilt uns mit, dass das Ergebnis von „ component.getElements “ nicht mit dem Snapshot übereinstimmt. Also führen wir diesen Test durch, indem wir das Eingabeelement in MyComponent .
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } } Der nächste Schritt besteht darin, der input Funktionalität hinzuzufügen, indem eine Funktion ausgeführt wird, wenn sich ihr Wert ändert. Wir tun dies, indem wir eine Funktion in der onChange Prop angeben.
Wir müssen zuerst den Snapshot ändern, damit der Test fehlschlägt.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;Ein Nachteil davon, zuerst den Schnappschuss zu modifizieren, ist, dass die Reihenfolge der Requisiten (oder Attribute) wichtig ist.
Jest sortiert die in der expect empfangenen Requisiten alphabetisch, bevor er sie mit dem Snapshot verifiziert. Also sollten wir sie in dieser Reihenfolge angeben.
Nach dem Ausführen des Tests erhalten wir folgenden Fehler:
Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ] Damit dieser Test bestanden wird, können wir einfach eine leere Funktion für onChange .
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } } Dann stellen wir sicher, dass sich der Zustand der Komponente ändert, nachdem das onChange Ereignis ausgelöst wurde.
Dazu erstellen wir einen neuen Komponententest, der die onChange Funktion in der Eingabe aufruft, indem er ein Ereignis übergibt, um ein echtes Ereignis in der Benutzeroberfläche nachzuahmen.
Dann überprüfen wir input enthält .
// MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });Wir erhalten jetzt den folgenden Fehler.
Expected value to be defined, instead received undefined Dies zeigt an, dass die Komponente keine Eigenschaft im Zustand namens input hat.
Wir machen den Test bestanden, indem wir diesen Eintrag in den Zustand der Komponente setzen.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }Dann müssen wir sicherstellen, dass im neuen Statuseintrag ein Wert festgelegt ist. Wir erhalten diesen Wert aus dem Ereignis.
Lassen Sie uns also einen Test erstellen, der sicherstellt, dass der Status diesen Wert enthält.
// MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""Wir führen diesen Test schließlich durch, indem wir den Wert aus dem Ereignis abrufen und als Eingabewert festlegen.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }Nachdem wir sichergestellt haben, dass alle Tests bestanden wurden, können wir unseren Code umgestalten.
Wir können die in der onChange übergebene Funktion in eine neue Funktion namens updateState .
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }Wir haben jetzt eine einfache React.js-Komponente, die mit TDD erstellt wurde.
Zusammenfassung
In diesem Beispiel haben wir versucht, reines TDD zu verwenden, indem wir jeden Schritt befolgt haben und so wenig Code wie möglich geschrieben haben, um Tests nicht zu bestehen und zu bestehen.
Einige der Schritte mögen unnötig erscheinen und wir könnten versucht sein, sie zu überspringen. Wann immer wir jedoch einen Schritt überspringen, verwenden wir am Ende eine weniger reine Version von TDD.
Die Verwendung eines weniger strengen TDD-Prozesses ist ebenfalls gültig und kann gut funktionieren.
Meine Empfehlung für Sie ist, keine Schritte zu überspringen und kein schlechtes Gewissen zu haben, wenn es Ihnen schwer fällt. TDD ist eine Technik, die nicht leicht zu beherrschen ist, aber es lohnt sich auf jeden Fall.
Wenn Sie daran interessiert sind, mehr über TDD und die damit verbundene verhaltensgesteuerte Entwicklung (BDD) zu erfahren, lesen Sie Your Boss Won't Appreciate TDD von Toptaler Ryan Wilcox.
