Cum React Components facilitează testarea UI
Publicat: 2022-03-11Testarea back-end-urilor este ușoară. Îți iei limba pe care o alegeți, o asociați cu framework-ul preferat, scrieți câteva teste și apăsați „run”. Consola ta spune „Da! Funcționează!" Serviciul dvs. de integrare continuă îți rulează testele la fiecare împingere, viața este grozavă.
Sigur, dezvoltarea bazată pe teste (TDD) este ciudată la început, dar un mediu previzibil, mai mulți teste, instrumente de testare integrate în cadre și asistență continuă pentru integrare, fac viața mai ușoară. Acum cinci ani credeam că testele sunt soluția pentru fiecare problemă pe care am avut-o vreodată.
Apoi Backbone a devenit mare.
Am trecut cu toții la MVC front-end. Backend-urile noastre testabile au devenit servere de baze de date glorificate. Codul nostru cel mai complicat a fost mutat în browser. Și aplicațiile noastre nu mai erau testabile în practică.
Asta pentru că testarea codului front-end și a componentelor UI este destul de dificilă.
Nu e chiar atât de rău dacă tot ce ne dorim este să verificăm dacă modelele noastre se comportă bine. Sau, apelarea unei funcții va schimba valoarea corectă. Tot ce trebuie să facem pentru testarea unității React este:
- Scrieți module bine formate, izolate.
- Utilizați teste Jasmine sau Mocha (sau orice altceva) pentru a rula funcții.
- Utilizați un alergător de testare, cum ar fi Karma sau Chutzpah.
Asta e. Codul nostru este testat unitar.
Pe vremuri, rularea testelor front-end era partea grea. Fiecare cadru avea propriile idei și, în cele mai multe cazuri, ați ajuns cu o fereastră de browser pe care o reîmprospătați manual de fiecare dată când doriți să rulați testele. Desigur, vei uita mereu. Cel puțin, știu că am făcut-o.
În 2012, Vojta Jina a lansat Karma runner (numit la acea vreme Testacular). Cu Karma, testarea front-end devine un cetățean deplin al lanțului de instrumente. Testele noastre React rulează într-un terminal sau pe un server de integrare continuă, se rulează singure atunci când modificăm un fișier și putem chiar testa codul nostru în mai multe browsere în același timp.
Ce ne-am putea dori mai mult? Ei bine, pentru a testa codul nostru front-end.
Testarea front-end necesită mai mult decât teste unitare
Testarea unitară este grozavă: este cea mai bună modalitate de a vedea dacă un algoritm face ceea ce trebuie de fiecare dată sau de a verifica logica noastră de validare a intrărilor, transformările datelor sau orice altă operațiune izolată. Testarea unitară este perfectă pentru elementele fundamentale.
Dar codul front-end nu este despre manipularea datelor. Este vorba despre evenimentele utilizatorilor și redarea vizualizărilor potrivite la momentul potrivit. Front-end-urile sunt despre utilizatori.
Iată ce vrem să putem face:
- Testați evenimentele utilizatorului React
- Testați răspunsul la aceste evenimente
- Asigurați-vă că se redă lucrurile potrivite la momentul potrivit
- Rulați teste în multe browsere
- Reluați testele asupra modificărilor fișierelor
- Lucrați cu sisteme de integrare continuă precum Travis
În cei zece ani în care am făcut asta, nu am găsit o modalitate decentă de a testa interacțiunea utilizatorului și de a vizualiza redarea până când nu am început să mă uit la React.
React Unit Testing: Componente UI
Reacționează este cel mai simplu mod de a atinge aceste obiective. În parte, din cauza modului în care ne obligă să proiectăm aplicații folosind modele testabile, în parte, pentru că există utilitare de testare React fantastice.
Dacă nu ați mai folosit niciodată React, ar trebui să consultați cartea mea React+d3.js . Este orientat spre vizualizări, dar mi s-a spus că este „o introducere ușoară minunată” pentru React.
React ne obligă să construim totul ca „componente”. Vă puteți gândi la componentele React ca widget-uri sau ca bucăți de HTML cu o anumită logică. Ei urmează multe dintre cele mai bune principii ale programării funcționale, cu excepția faptului că sunt obiecte.
De exemplu, având în vedere același set de parametri, o componentă React va reda întotdeauna aceeași ieșire. Indiferent de câte ori este redat, indiferent cine îl redă, indiferent unde plasăm rezultatul. Întotdeauna la fel. Drept urmare, nu trebuie să realizăm schele complexe pentru a testa componentele React. Le pasă doar de proprietățile lor, nu este necesară urmărirea variabilelor globale și a obiectelor de configurare.
Obținem acest lucru în mare parte evitând starea. Ai numi această transparență referențială în programarea funcțională. Nu cred că există un nume special pentru acest lucru în React, dar documentele oficiale recomandă evitarea folosirii statului pe cât posibil.
Când vine vorba de testarea interacțiunilor utilizatorilor, React ne acoperă cu evenimente legate de apeluri inverse. Este ușor să configurați spioni de testare și să vă asigurați că un eveniment de clic apelează funcția corectă. Și deoarece componentele React se redau singure, putem declanșa un eveniment de clic și putem verifica HTML pentru modificări. Acest lucru funcționează deoarece unei componente React îi pasă doar de ea însăși. Un clic aici nu schimbă lucrurile acolo . Nu va trebui niciodată să avem de-a face cu un cuib de handlere de evenimente, doar cu apeluri de funcții bine definite.
A, și pentru că React este magic, nu trebuie să ne facem griji pentru DOM. React folosește așa-numitul DOM virtual pentru a reda componente într-o variabilă JavaScript. Și o referință la DOM-ul virtual este tot ce avem nevoie pentru a testa componentele React, într-adevăr.
E destul de dulce.
TestUtils
de la React
React vine cu o suită de TestUtils
. Există chiar și un testator recomandat, numit Jest, dar nu-mi place. O sa explic de ce peste putin. În primul rând, TestUtils
.
Le obținem făcând ceva de genul require('react/addons').addons.TestUtils
. Acesta este punctul nostru de intrare pentru a testa interacțiunile utilizatorilor și a verifica rezultatul.
React TestUtils
ne permite să redăm o componentă React punând DOM-ul acesteia într-o variabilă, în loc să o inserăm într-o pagină. De exemplu, pentru a reda o componentă React, am face ceva de genul acesta:
var component = TestUtils.renderIntoDocument( <MyComponent /> );
Apoi putem folosi TestUtils
pentru a verifica dacă toți copiii au fost randați. Ceva de genul:
var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );
findRenderedDOMComponentWithTag
va face ceea ce pare: treceți prin copii, găsiți componenta pe care o căutăm și returnați-o. Valoarea returnată se va comporta ca o componentă React.
Apoi putem folosi getDOMNode()
pentru a accesa elementul DOM brut și a testa valorile acestuia. Pentru a verifica dacă o etichetă h1
din componentă spune „Un titlu” , am scrie asta:
expect(h1.getDOMNode().textContent) .toEqual("A title");
Laolaltă, testul complet ar arăta astfel:
it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });
Partea grozavă este că TestUtils ne permite să declanșăm și evenimente de utilizator. Pentru un eveniment de clic, am scrie ceva de genul acesta:
var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);
Aceasta simulează un clic și declanșează orice ascultători potențiali, care ar trebui să fie metode componente care modifică fie rezultatul, starea sau ambele. Acei ascultători pot apela o funcție pe o componentă părinte, dacă este necesar.
Toate cazurile sunt simplu de testat: starea schimbată este în component.state
, putem accesa ieșirea cu funcții normale DOM și apeluri de funcție cu spioni.
De ce nu gluma?
Documentația oficială a lui React recomandă utilizarea https://facebook.github.io/jest/ ca ruler de testare și cadru de testare React. Jest este construit pe Jasmine și folosește aceeași sintaxă. Pe lângă tot ceea ce obțineți de la Jasmine, Jest bate joc de tot, cu excepția componentei pe care o testăm. Acest lucru este fantastic în teorie, dar mi se pare enervant. Orice nu am implementat încă, sau care provine dintr-o altă parte a bazei de cod, este pur și simplu undefined
. Deși acest lucru este în regulă în multe cazuri, poate duce la erori liniștite.
Am avut probleme la testarea unui eveniment de clic, de exemplu. Indiferent ce am încercat, pur și simplu nu și-a numit ascultătorul. Apoi mi-am dat seama că funcția a fost batjocorită de Jest și nu mi-a spus niciodată asta.
Dar cea mai gravă ofensă a lui Jest, de departe, era că nu avea un mod de ceas pentru a testa automat noile modificări. Am putea să-l rulăm o dată, să obținem rezultate ale testelor și atât. (Îmi place să-mi rulez testele în fundal în timp ce lucrez. Altfel uit să le rulez.) În prezent, aceasta nu mai este o problemă.
Ah, și Jest nu acceptă rularea testelor React în mai multe browsere. Aceasta este o problemă mai puțin decât era odinioară, dar simt că este o caracteristică importantă pentru acea ocazie rară în care o defecțiune se întâmplă doar într-o anumită versiune de Chrome...
Nota editorului: De când acest articol a fost scris inițial, Jest s-a îmbunătățit substanțial. Puteți citi tutorialul nostru cel mai recent, React Unit Testing Using Enzyme and Jest și puteți decide singur dacă testarea Jest este la îndemână în prezent.
Testarea React: un exemplu integrat
Oricum, am văzut cum ar trebui să funcționeze teoretic un test front-end React bun. Să punem în practică cu un exemplu scurt.
Vom vizualiza diferite moduri de a genera numere aleatoare folosind o componentă scatterplot realizată cu React și d3.js. Codul și demonstrația sa sunt, de asemenea, pe Github.
Vom folosi Karma ca runner de testare, Mocha ca cadru de testare și Webpack ca încărcător de module.
Pregatirea
Fișierele noastre sursă vor merge într-un director <root>/src
, iar noi vom pune teste într-un director <root>/src/__tests__
. Ideea este că putem pune mai multe directoare în src
, câte unul pentru fiecare componentă majoră și fiecare cu propriile fișiere de testare. Gruparea codului sursă și a fișierelor de testare în acest fel facilitează reutilizarea componentelor React în diferite proiecte.
Cu structura de directoare instalată, putem instala dependențe astfel:

$ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect
Dacă ceva nu reușește să se instaleze, încercați să rulați din nou acea parte a instalării. NPM uneori eșuează în moduri care dispar la o reluare.
Fișierul nostru package.json
ar trebui să arate astfel când terminăm:
// 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" } }
După o anumită configurare, vom putea rula teste fie cu npm test
fie cu karma start
.
Config
Nu este mare lucru în configurație. Trebuie să ne asigurăm că Webpack știe cum să ne găsească codul și că Karma știe cum să ruleze testele.
Am pus două linii de JavaScript într-un fișier ./tests.webpack.js
pentru a ajuta Karma și Webpack să se joace împreună:
// tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);
Acest lucru îi spune lui Webpack să ia în considerare orice cu un sufix -test
să facă parte din suita de teste.
Configurarea Karma necesită ceva mai multă muncă:
// 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 } }); };
Cele mai multe dintre aceste linii provin dintr-o configurație implicită Karma. Am folosit browsers
pentru a spune că testele ar trebui să ruleze în Chrome, frameworks
pentru a specifica ce cadru de testare folosim și singleRun
pentru ca testele să ruleze o singură dată în mod implicit. Puteți menține karma să ruleze în fundal cu karma start --no-single-run
.
Cele trei sunt evidente. Lucrurile Webpack sunt mai interesante.
Deoarece Webpack se ocupă de arborele de dependență al codului nostru, nu trebuie să specificăm toate fișierele noastre în matricea de files
. Avem nevoie doar de tests.webpack.js
, care apoi necesită toate fișierele necesare.
Folosim setarea webpack
pentru a-i spune lui Webpack ce să facă. Într-un mediu normal, această parte ar merge într-un fișier webpack.config.js
.
De asemenea, îi spunem lui Webpack să folosească babel-loader
pentru JavaScript-urile noastre. Acest lucru ne oferă toate funcțiile noi de la ECMAScript2015 și JSX de la React.
Cu configurația webpackServer
, îi spunem lui Webpack să nu imprime nicio informație de depanare. Ar strica doar rezultatul nostru de testare.
O componentă React și un test
Cu o suită de teste care rulează, restul este simplu. Trebuie să facem o componentă care acceptă o serie de coordonate aleatoare și creează un element <svg>
cu o grămadă de puncte.
Urmând cele mai bune practici de testare React, adică practica standard TDD, vom scrie mai întâi testul, apoi componenta React reală. Să începem cu un fișier de teste vanilie în 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()}; }); });
Mai întâi avem nevoie de React, TestUtils, d3.js, biblioteca expect
și codul pe care îl testăm. Apoi facem o nouă suită de teste cu describe
și creăm câteva date aleatorii.
Pentru primul nostru test, să ne asigurăm că ScatterPlot
redă un titlu. Testul nostru merge în blocul 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"); });
Majoritatea testelor vor urma același model:
- Face.
- Găsiți un nod specific.
- Verificați conținutul.
După cum am văzut mai devreme, renderIntoDocument
redă componenta noastră, findRenderedDOMComponentWithTag
găsește partea specifică pe care o testăm și getDOMNode
ne oferă acces DOM brut.
La început testul nostru va eșua. Pentru a trece, trebuie să scriem componenta care redă o etichetă de titlu:
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;
Asta e. Componenta ScatterPlot
redă un <div>
cu o etichetă <h1>
care conține textul așteptat, iar testul nostru va trece. Da, este mai lung decât HTML, dar suportați-mă.
Desenați restul bufniței
Puteți vedea restul exemplului nostru pe GitHub, așa cum am menționat mai sus. Vom sări peste descrierea pas cu pas în acest articol, dar procesul general este același ca mai sus. Totuși, vreau să vă arăt un test mai interesant. Un test care asigură că toate punctele de date apar pe diagramă:
// 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); });
La fel ca inainte. Redați, găsiți noduri, verificați rezultatul. Partea interesantă aici este desenarea acelor noduri DOM. Adăugăm ceva magie d3.js la componenta ScatterPlot
, astfel:
// 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)]); }, ...
Folosim componentWillMount
pentru a configura scalele d3 goale pentru domeniile X și Y și componentWillReceiveProps
pentru a ne asigura că sunt actualizate atunci când ceva se schimbă. Apoi update_d3
se asigură că setează domain
și range
pentru ambele scale.
Vom folosi cele două scale pentru a traduce valori aleatorii din setul nostru de date și pozițiile din imagine. Majoritatea generatoarelor aleatoare returnează numere în intervalul [0,1] , care este prea mic pentru a fi văzut ca pixeli.
Apoi adăugăm punctele la metoda de randare a componentei noastre:
// 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> ); }
Acest cod trece prin matricea this.props.data
și adaugă un element <circle>
pentru fiecare punct de date. Simplu.
Dacă doriți să aflați mai multe despre combinarea React și d3.js pentru a crea componente de vizualizare a datelor, acesta este un alt motiv excelent pentru a consulta cartea mea, React+d3.js .
Testarea automată a componentelor React: mai ușoară decât pare
Acesta este tot ce trebuie să știm despre scrierea componentelor front-end testabile cu React. Pentru a vedea mai multe componente de testare a codului React, consultați exemplul de bază de cod de testare React pe Github, așa cum s-a menționat mai sus.
Am invatat ca:
- React ne obligă să modularizăm și să încapsulăm.
- Acest lucru face testarea React UI ușor de automatizat.
- Testele unitare nu sunt suficiente pentru front-end.
- Karma este un mare alergător de teste.
- Jest are potențial, dar nu este încă acolo. (Sau poate acum este.)
Dacă ți-a plăcut acest articol, urmărește-mă pe Twitter și lasă un comentariu mai jos. Mulțumim pentru lectură și testare React fericită!