Wie React-Komponenten das Testen der Benutzeroberfläche vereinfachen
Veröffentlicht: 2022-03-11Das Testen von Backends ist einfach. Sie nehmen die Sprache Ihrer Wahl, koppeln sie mit Ihrem bevorzugten Framework, schreiben einige Tests und klicken auf „Ausführen“. Ihre Konsole sagt „Juhu! Es klappt!" Ihr Continuous Integration Service führt Ihre Tests bei jedem Push durch, das Leben ist großartig.
Sicher, testgetriebene Entwicklung (TDD) ist zunächst seltsam, aber eine vorhersehbare Umgebung, mehrere Test-Runner, in Frameworks integrierte Testtools und kontinuierliche Integrationsunterstützung machen das Leben einfach. Vor fünf Jahren dachte ich, Tests seien die Lösung für jedes Problem, das ich je hatte.
Dann wurde Backbone groß.
Wir sind alle auf Front-End-MVC umgestiegen. Unsere testbaren Backends wurden zu verherrlichten Datenbankservern. Unser kompliziertester Code wurde in den Browser verschoben. Und unsere Apps waren in der Praxis nicht mehr testbar.
Das liegt daran, dass das Testen von Front-End-Code und UI-Komponenten ziemlich schwierig ist.
Es ist nicht so schlimm, wenn wir nur überprüfen wollen, ob sich unsere Modelle gut verhalten. Oder dass der Aufruf einer Funktion den richtigen Wert ändert. Alles, was wir für React Unit Tests tun müssen, ist:
- Schreiben Sie wohlgeformte, isolierte Module.
- Verwenden Sie Jasmine- oder Mocha-Tests (oder was auch immer), um Funktionen auszuführen.
- Verwenden Sie einen Testläufer wie Karma oder Chuzpah.
Das ist es. Unser Code ist einheitengetestet.
Früher war das Ausführen von Front-End-Tests der schwierige Teil. Jedes Framework hatte seine eigenen Ideen und in den meisten Fällen endete es mit einem Browserfenster, das Sie jedes Mal manuell aktualisieren mussten, wenn Sie die Tests ausführen wollten. Natürlich würdest du es immer vergessen. Zumindest weiß ich, dass ich es getan habe.
2012 veröffentlichte Vojta Jina den Karma Runner (damals Testacular genannt). Mit Karma wird das Front-End-Testen zu einem vollwertigen Bestandteil der Toolkette. Unsere React-Tests werden in einem Terminal oder auf einem Continuous-Integration-Server ausgeführt, sie führen sich selbst erneut aus, wenn wir eine Datei ändern, und wir können unseren Code sogar in mehreren Browsern gleichzeitig testen.
Was könnten wir uns mehr wünschen? Nun, um unseren Front-End-Code tatsächlich zu testen.
Front-End-Tests erfordern mehr als nur Unit-Tests
Komponententests sind großartig: Es ist der beste Weg, um zu sehen, ob ein Algorithmus jedes Mal das Richtige tut, oder um unsere Eingabevalidierungslogik, Datentransformationen oder andere isolierte Vorgänge zu überprüfen. Unit-Tests eignen sich perfekt für Grundlagen.
Aber beim Front-End-Code geht es nicht darum, Daten zu manipulieren. Es geht um Benutzerereignisse und das Rendern der richtigen Ansichten zur richtigen Zeit. Bei Frontends geht es um Benutzer.
Folgendes wollen wir können:
- Testen Sie React-Benutzerereignisse
- Testen Sie die Reaktion auf diese Ereignisse
- Stellen Sie sicher, dass die richtigen Dinge zur richtigen Zeit gerendert werden
- Führen Sie Tests in vielen Browsern durch
- Führen Sie Tests zu Dateiänderungen erneut aus
- Arbeiten Sie mit kontinuierlichen Integrationssystemen wie Travis
In den zehn Jahren, in denen ich dies tue, hatte ich keinen anständigen Weg gefunden, die Benutzerinteraktion und das Rendering von Ansichten zu testen, bis ich anfing, React zu untersuchen.
React Unit Testing: UI-Komponenten
React ist der einfachste Weg, diese Ziele zu erreichen. Zum Teil, weil es uns zwingt, Apps mit testbaren Mustern zu entwickeln, zum Teil, weil es fantastische React-Test-Utilities gibt.
Wenn Sie React noch nie benutzt haben, sollten Sie sich mein Buch React+d3.js ansehen . Es ist auf Visualisierungen ausgerichtet, aber mir wurde gesagt, es sei „ein tolles, leichtes Intro“ für React.
React zwingt uns, alles als „Komponenten“ zu bauen. Sie können sich React-Komponenten als Widgets oder als HTML-Blöcke mit etwas Logik vorstellen. Sie folgen vielen der besten Prinzipien der funktionalen Programmierung, außer dass sie Objekte sind.
Beispielsweise liefert eine React-Komponente bei gleichem Parametersatz immer die gleiche Ausgabe. Egal wie oft es gerendert wird, egal wer es macht, egal wo wir die Ausgabe platzieren. Immer gleich. Infolgedessen müssen wir kein komplexes Scaffolding durchführen, um React-Komponenten zu testen. Sie kümmern sich nur um ihre Eigenschaften, es ist keine Verfolgung globaler Variablen und Konfigurationsobjekte erforderlich.
Wir erreichen dies zu einem großen Teil, indem wir Staat vermeiden. Sie würden diese referenzielle Transparenz in der funktionalen Programmierung nennen. Ich glaube nicht, dass es dafür in React einen speziellen Namen gibt, aber die offiziellen Dokumente empfehlen, die Verwendung von state so weit wie möglich zu vermeiden.
Wenn es um das Testen von Benutzerinteraktionen geht, hat uns React mit Ereignissen abgedeckt, die an Funktionsrückrufe gebunden sind. Es ist einfach, Testspione einzurichten und sicherzustellen, dass ein Klickereignis die richtige Funktion aufruft. Und da React-Komponenten sich selbst rendern, können wir einfach ein Klick-Ereignis auslösen und den HTML-Code auf Änderungen überprüfen. Das funktioniert, weil sich eine React-Komponente nur um sich selbst kümmert. Ein Klick hier ändert dort nichts . Wir müssen uns nie mit einer Verschachtelung von Event-Handlern befassen, sondern nur mit wohldefinierten Funktionsaufrufen.
Oh, und weil React magisch ist, müssen wir uns keine Sorgen um das DOM machen. React verwendet das sogenannte virtuelle DOM, um Komponenten in eine JavaScript-Variable zu rendern. Und ein Verweis auf das virtuelle DOM ist alles, was wir wirklich brauchen, um React-Komponenten zu testen.
Es ist ziemlich süß.
TestUtils
von React
React wird mit einer Reihe integrierter TestUtils
. Es gibt sogar einen empfohlenen Testläufer namens Jest, aber ich mag ihn nicht. Warum, erkläre ich gleich. Zuerst die TestUtils
.
Wir erhalten sie, indem wir so etwas wie require('react/addons').addons.TestUtils
. Dies ist unser Einstiegspunkt, um Benutzerinteraktionen zu testen und die Ausgabe zu überprüfen.
Mit den React TestUtils
wir eine React-Komponente rendern, indem wir ihr DOM in eine Variable einfügen, anstatt es in eine Seite einzufügen. Um beispielsweise eine React-Komponente zu rendern, würden wir etwa so vorgehen:
var component = TestUtils.renderIntoDocument( <MyComponent /> );
Dann können wir TestUtils
verwenden, um zu überprüfen, ob alle untergeordneten Elemente gerendert wurden. Etwas wie das:
var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );
findRenderedDOMComponentWithTag
wird das tun, wonach es sich anhört: die untergeordneten Elemente durchgehen, die gesuchte Komponente finden und sie zurückgeben. Der zurückgegebene Wert verhält sich wie eine React-Komponente.
Wir können dann getDOMNode()
verwenden, um auf das rohe DOM-Element zuzugreifen und seine Werte zu testen. Um zu überprüfen, ob ein h1
-Tag in der Komponente „A title“ lautet, schreiben wir Folgendes:
expect(h1.getDOMNode().textContent) .toEqual("A title");
Zusammengenommen würde der vollständige Test so aussehen:
it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });
Der coole Teil ist, dass TestUtils uns auch Benutzerereignisse auslösen lässt. Für ein Klickereignis würden wir etwa so schreiben:
var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);
Dies simuliert einen Klick und löst potenzielle Listener aus, bei denen es sich um Komponentenmethoden handeln sollte, die entweder die Ausgabe, den Zustand oder beides ändern. Diese Listener können bei Bedarf eine Funktion für eine übergeordnete Komponente aufrufen.
Alle Fälle sind einfach zu testen: Der geänderte Zustand befindet sich in component.state
, wir können mit normalen DOM-Funktionen auf die Ausgabe zugreifen und mit Spies auf Funktionsaufrufe.
Warum nicht scherzen?
Die offizielle Dokumentation von React empfiehlt die Verwendung von https://facebook.github.io/jest/ als Testrunner und React-Testframework. Jest basiert auf Jasmine und verwendet die gleiche Syntax. Zusätzlich zu allem, was Sie von Jasmine bekommen, verspottet Jest auch alles außer der Komponente, die wir testen. Theoretisch ist das fantastisch, aber ich finde es nervig. Alles, was wir noch nicht implementiert haben oder das aus einem anderen Teil der Codebasis stammt, ist einfach undefined
. Während dies in vielen Fällen in Ordnung ist, kann es zu stillschweigenden Fehlern führen.
Ich hatte zum Beispiel Probleme beim Testen eines Klickereignisses. Egal, was ich versuchte, es rief seinen Zuhörer einfach nicht an. Dann wurde mir klar, dass die Funktion von Jest verspottet wurde und mir das nie gesagt hat.
Aber Jests schlimmstes Vergehen war früher, dass es keinen Überwachungsmodus hatte, um neue Änderungen automatisch zu testen. Wir könnten es einmal ausführen, Testergebnisse erhalten, und das war's. (Ich lasse meine Tests gerne im Hintergrund laufen, während ich arbeite. Sonst vergesse ich, sie auszuführen.) Heutzutage ist das kein Problem mehr.
Oh, und Jest unterstützt nicht das Ausführen von React-Tests in mehreren Browsern. Dies ist weniger ein Problem als früher, aber ich denke, es ist ein wichtiges Feature für den seltenen Fall, dass ein Heisenbug nur in einer bestimmten Version von Chrome auftritt …
Anmerkung des Herausgebers: Seit dieser Artikel ursprünglich geschrieben wurde, hat sich Jest erheblich verbessert. Sie können unser neueres Tutorial React Unit Testing Using Enzyme and Jest lesen und selbst entscheiden, ob Jest-Tests heutzutage der Aufgabe gewachsen sind.
Reaktionstest: Ein integriertes Beispiel
Wie auch immer, wir haben gesehen, wie ein guter Front-End-React-Test theoretisch funktionieren sollte. Lassen Sie es uns mit einem kurzen Beispiel in die Tat umsetzen.
Wir werden verschiedene Möglichkeiten zur Generierung von Zufallszahlen mithilfe einer Scatterplot-Komponente visualisieren, die mit React und d3.js erstellt wurde. Der Code und seine Demo sind auch auf Github.
Wir werden Karma als Test-Runner, Mocha als Test-Framework und Webpack als Modul-Loader verwenden.
Die Einrichtung
Unsere Quelldateien werden in einem <root>/src
-Verzeichnis abgelegt, und wir legen Tests in einem <root>/src/__tests__
Verzeichnis ab. Die Idee ist, dass wir mehrere Verzeichnisse innerhalb von src
ablegen können, eines für jede Hauptkomponente und jedes mit seinen eigenen Testdateien. Das Bündeln von Quellcode und Testdateien auf diese Weise erleichtert die Wiederverwendung von React-Komponenten in verschiedenen Projekten.

Mit der vorhandenen Verzeichnisstruktur können wir Abhängigkeiten wie folgt installieren:
$ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect
Wenn etwas nicht installiert werden kann, versuchen Sie, diesen Teil der Installation erneut auszuführen. NPM schlägt manchmal auf eine Weise fehl, die bei einer erneuten Ausführung verschwindet.
Unsere package.json
-Datei sollte so aussehen, wenn wir fertig sind:
// package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }
Nach einiger Konfiguration können wir Tests entweder mit npm test
oder karma start
ausführen.
Die Konfig
An der Konfiguration ist nicht viel dran. Wir müssen sicherstellen, dass Webpack weiß, wie es unseren Code findet, und dass Karma weiß, wie es die Tests durchführt.
Wir haben zwei JavaScript-Zeilen in eine ./tests.webpack.js
, damit Karma und Webpack zusammen spielen können:
// tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);
Dies weist Webpack an, alles mit einem -test
-Suffix als Teil der Testsuite zu betrachten.
Das Konfigurieren von Karma erfordert etwas mehr Arbeit:
// karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };
Die meisten dieser Zeilen stammen aus einer standardmäßigen Karma-Konfiguration. Wir haben browsers
verwendet, um anzugeben, dass Tests in Chrome ausgeführt werden sollen, frameworks
, um anzugeben, welches Testframework wir verwenden, und singleRun
, um Tests standardmäßig nur einmal auszuführen. Mit karma start --no-single-run
können Sie Karma im Hintergrund laufen lassen.
Diese drei sind offensichtlich. Das Webpack-Zeug ist interessanter.
Da Webpack den Abhängigkeitsbaum unseres Codes verwaltet, müssen wir nicht alle unsere Dateien im files
-Array angeben. Wir brauchen nur tests.webpack.js
, die dann alle notwendigen Dateien benötigt.
Wir verwenden die webpack
Einstellung, um Webpack mitzuteilen, was zu tun ist. In einer normalen Umgebung würde dieser Teil in einer webpack.config.js
-Datei landen.
Wir weisen Webpack auch an, den babel-loader
für unsere JavaScripts zu verwenden. Dies gibt uns all die schicken neuen Funktionen von ECMAScript2015 und JSX von React.
Mit der webpackServer
Konfiguration weisen wir Webpack an, keine Debug-Informationen zu drucken. Es würde unsere Testausgabe nur verderben.
Eine Reaktionskomponente und ein Test
Mit einer laufenden Testsuite ist der Rest einfach. Wir müssen eine Komponente erstellen, die ein Array zufälliger Koordinaten akzeptiert und ein <svg>
-Element mit einer Reihe von Punkten erstellt.
Gemäß den Best Practices für React-Tests – dh Standard-TDD-Praxis – schreiben wir zuerst den Test und dann die eigentliche React-Komponente. Beginnen wir mit einer Vanilla-Testdatei in src/__tests__/
:
// ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });
Zuerst benötigen wir React, seine TestUtils, expect
, die Expect-Bibliothek und den Code, den wir testen. Dann erstellen wir eine neue Testsuite mit describe
und erstellen einige zufällige Daten.
Lassen Sie uns für unseren ersten Test sicherstellen, dass ScatterPlot
einen Titel rendert. Unser Test geht in den describe
:
// ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });
Die meisten Tests folgen demselben Muster:
- Machen.
- Bestimmten Knoten finden.
- Inhalt prüfen.
Wie wir zuvor gesehen haben, renderIntoDocument
rendert unsere Komponente, findRenderedDOMComponentWithTag
findet den spezifischen Teil, den wir testen, und getDOMNode
gibt uns einen rohen DOM-Zugriff.
Zunächst wird unser Test fehlschlagen. Damit es durchgeht, müssen wir die Komponente schreiben, die ein Titel-Tag rendert:
var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;
Das ist es. Die ScatterPlot
Komponente rendert ein <div>
mit einem <h1>
-Tag, das den erwarteten Text enthält, und unser Test wird bestanden. Ja, es ist länger als nur HTML, aber ertragen Sie mich.
Zeichne den Rest der Eule
Sie können den Rest unseres Beispiels auf GitHub sehen, wie oben erwähnt. Wir werden es in diesem Artikel überspringen, es Schritt für Schritt zu beschreiben, aber der allgemeine Prozess ist derselbe wie oben. Ich möchte Ihnen jedoch einen interessanteren Test zeigen. Ein Test, der sicherstellt, dass alle Datenpunkte im Diagramm angezeigt werden:
// ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });
Das Gleiche wie vorher. Rendern, Knoten finden, Ergebnis prüfen. Der interessante Teil hier ist das Zeichnen dieser DOM-Knoten. Wir fügen der ScatterPlot
Komponente etwas d3.js-Magie hinzu, wie folgt:
// ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...
Wir verwenden „ componentWillMount
“, um leere d3-Skalen für die X- und Y -Domänen einzurichten, und „ componentWillReceiveProps
“, um sicherzustellen, dass sie aktualisiert werden, wenn sich etwas ändert. Dann stellt update_d3
sicher, dass die domain
und der range
für beide Skalen festgelegt werden.
Wir verwenden die beiden Skalen, um zwischen zufälligen Werten in unserem Datensatz und Positionen auf dem Bild zu übersetzen. Die meisten Zufallsgeneratoren geben Zahlen im Bereich [0,1] zurück, der zu klein ist, um als Pixel erkannt zu werden.
Dann fügen wir die Punkte zur Rendermethode unserer Komponente hinzu:
// ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }
Dieser Code durchläuft das Array this.props.data
und fügt ein <circle>
-Element für jeden Datenpunkt hinzu. Einfach.
Wenn Sie mehr über die Kombination von React und d3.js zur Erstellung von Datenvisualisierungskomponenten erfahren möchten, ist dies ein weiterer guter Grund, mein Buch React+d3.js zu lesen.
Automatisiertes Testen von Reaktionskomponenten: einfacher als es klingt
Das ist alles, was wir über das Schreiben testbarer Frontend-Komponenten mit React wissen müssen. Weitere React-Komponenten zum Testen von Code finden Sie in der Beispiel-Codebasis für React-Tests auf Github, wie oben erwähnt.
Das haben wir gelernt:
- React zwingt uns zur Modularisierung und Kapselung.
- Dies macht das Testen der React-Benutzeroberfläche einfach zu automatisieren.
- Unit-Tests reichen für Frontends nicht aus.
- Karma ist ein großartiger Testläufer.
- Jest hat Potenzial, ist aber noch nicht ganz da. (Oder vielleicht ist es das jetzt.)
Wenn Ihnen dieser Artikel gefallen hat, folgen Sie mir auf Twitter und hinterlassen Sie unten einen Kommentar. Danke fürs Lesen und viel Spaß beim React-Testen!