In che modo i componenti React semplificano i test dell'interfaccia utente
Pubblicato: 2022-03-11Testare i back-end è facile. Prendi la tua lingua preferita, la abbini al tuo framework preferito, scrivi alcuni test e premi "esegui". La tua console dice "Sì! Funziona!" Il tuo servizio di integrazione continua esegue i tuoi test su ogni spinta, la vita è fantastica.
Certo, lo sviluppo guidato dai test (TDD) è strano all'inizio, ma un ambiente prevedibile, più test runner, strumenti di test integrati nei framework e supporto di integrazione continua semplificano la vita. Cinque anni fa pensavo che i test fossero la soluzione a ogni problema che avessi mai avuto.
Poi Backbone è diventato grande.
Siamo passati tutti a MVC front-end. I nostri backend testabili sono diventati server di database glorificati. Il nostro codice più complicato è stato spostato nel browser. E le nostre app non erano più testabili nella pratica.
Questo perché testare il codice front-end e i componenti dell'interfaccia utente è piuttosto difficile.
Non è poi così male se tutto ciò che vogliamo è controllare che i nostri modelli si comportino bene. Oppure, la chiamata di una funzione cambierà il valore corretto. Tutto ciò che dobbiamo fare per il test dell'unità React è:
- Scrivi moduli ben formati e isolati.
- Usa i test Jasmine o Mocha (o qualsiasi altra cosa) per eseguire funzioni.
- Usa un corridore di prova, come Karma o Chutzpah.
Questo è tutto. Il nostro codice è testato per unità.
In passato, eseguire i test front-end era la parte difficile. Ogni framework aveva le sue idee e nella maggior parte dei casi ci si ritrovava con una finestra del browser che si aggiornava manualmente ogni volta che si desiderava eseguire i test. Certo, lo dimenticherai sempre. Almeno, so di averlo fatto.
Nel 2012, Vojta Jina ha rilasciato il corridore Karma (all'epoca chiamato Testacular). Con Karma, i test front-end diventano cittadini a pieno titolo della catena degli strumenti. I nostri test React vengono eseguiti in un terminale o su un server di integrazione continua, si rieseguono da soli quando modifichiamo un file e possiamo anche testare il nostro codice in più browser contemporaneamente.
Cosa potremmo desiderare di più? Bene, per testare effettivamente il nostro codice front-end.
I test front-end richiedono più di semplici test unitari
Il test unitario è ottimo: è il modo migliore per vedere se un algoritmo fa la cosa giusta ogni volta, o per controllare la nostra logica di convalida dell'input, o le trasformazioni dei dati, o qualsiasi altra operazione isolata. Il test unitario è perfetto per i fondamenti.
Ma il codice front-end non riguarda la manipolazione dei dati. Riguarda gli eventi degli utenti e il rendering delle viste giuste al momento giusto. I front-end riguardano gli utenti.
Ecco cosa vogliamo essere in grado di fare:
- Test Reagisci agli eventi utente
- Testare la risposta a quegli eventi
- Assicurati che le cose giuste vengano renderizzate al momento giusto
- Esegui test in molti browser
- Riesegui i test sulle modifiche ai file
- Lavora con sistemi di integrazione continua come Travis
Nei dieci anni in cui ho fatto questo, non avevo trovato un modo decente per testare l'interazione dell'utente e visualizzare il rendering fino a quando non ho iniziato a dare un'occhiata a React.
Test unitario di reazione: componenti dell'interfaccia utente
Reagire è il modo più semplice per raggiungere questi obiettivi. In parte, per il modo in cui ci obbliga a progettare app utilizzando modelli testabili, in parte perché ci sono fantastiche utilità di test React.
Se non hai mai usato React prima, dovresti dare un'occhiata al mio libro React+d3.js . È orientato alla visualizzazione, ma mi è stato detto che è "una fantastica introduzione leggera" per React.
React ci costringe a costruire tutto come "componenti". Puoi pensare ai componenti di React come widget o come blocchi di HTML con una certa logica. Seguono molti dei migliori principi della programmazione funzionale, tranne che sono oggetti.
Ad esempio, dato lo stesso set di parametri, un componente React visualizzerà sempre lo stesso output. Non importa quante volte viene renderizzato, non importa chi lo esegue, non importa dove posizioniamo l'output. Sempre lo stesso. Di conseguenza, non è necessario eseguire scaffolding complessi per testare i componenti React. Si preoccupano solo delle loro proprietà, non è richiesto il tracciamento delle variabili globali e degli oggetti di configurazione.
Raggiungiamo questo obiettivo in gran parte evitando lo stato. Chiamereste questa trasparenza referenziale nella programmazione funzionale. Non credo che ci sia un nome speciale per questo in React, ma i documenti ufficiali raccomandano di evitare il più possibile l'uso dello stato.
Quando si tratta di testare le interazioni degli utenti, React ci copre con eventi legati ai callback di funzioni. È facile impostare spie di prova e assicurarsi che un evento clic richiami la funzione corretta. E poiché i componenti di React vengono visualizzati da soli, possiamo semplicemente attivare un evento clic e controllare l'HTML per le modifiche. Funziona perché un componente React si preoccupa solo di se stesso. Un clic qui non cambia le cose là . Non dovremo mai avere a che fare con un nido di gestori di eventi, solo chiamate di funzioni ben definite.
Oh, e poiché React è magico, non dobbiamo preoccuparci del DOM. React utilizza il cosiddetto DOM virtuale per eseguire il rendering dei componenti in una variabile JavaScript. E un riferimento al DOM virtuale è tutto ciò di cui abbiamo bisogno per testare i componenti React, davvero.
È piuttosto dolce.
TestUtils
di React
React viene fornito con una suite di TestUtils
. C'è anche un test runner consigliato chiamato Jest, ma non mi piace. Ti spiego perché tra un po'. Innanzitutto, TestUtils
.
Li otteniamo facendo qualcosa come require('react/addons').addons.TestUtils
. Questo è il nostro punto di ingresso per testare le interazioni degli utenti e controllare l'output.
React TestUtils
ci consente di eseguire il rendering di un componente React inserendo il suo DOM in una variabile, invece di inserirlo in una pagina. Ad esempio, per eseguire il rendering di un componente React, faremmo qualcosa del genere:
var component = TestUtils.renderIntoDocument( <MyComponent /> );
Quindi possiamo usare TestUtils
per verificare se tutti i bambini sono stati renderizzati. Qualcosa come questo:
var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );
findRenderedDOMComponentWithTag
farà quello che sembra: esaminare i bambini, trovare il componente che stiamo cercando e restituirlo. Il valore restituito si comporterà come un componente React.
Possiamo quindi utilizzare getDOMNode()
per accedere all'elemento DOM grezzo e testarne i valori. Per verificare che un tag h1
nel componente indichi "A title" , scriviamo questo:
expect(h1.getDOMNode().textContent) .toEqual("A title");
Messo insieme, il test completo sarebbe simile a questo:
it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });
La parte interessante è che TestUtils ci consente anche di attivare eventi utente. Per un evento di clic, scriveremmo qualcosa del genere:
var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);
Questo simula un clic e attiva tutti i potenziali ascoltatori, che dovrebbero essere metodi componenti che modificano l'output, lo stato o entrambi. Tali ascoltatori possono chiamare una funzione su un componente padre, se necessario.
Tutti i casi sono semplici da testare: lo stato modificato è in component.state
, possiamo accedere all'output con le normali funzioni DOM e le chiamate di funzione con spie.
Perché non scherzo?
La documentazione ufficiale di React consiglia di utilizzare https://facebook.github.io/jest/ come test runner e framework di test React. Jest è basato su Jasmine e utilizza la stessa sintassi. Oltre a tutto ciò che ottieni da Jasmine, Jest prende in giro anche tutto tranne il componente che stiamo testando. Questo è fantastico in teoria, ma lo trovo fastidioso. Tutto ciò che non abbiamo ancora implementato, o che proviene da una parte diversa della base di codice, è semplicemente undefined
. Anche se questo va bene in molti casi, può portare a bug che falliscono silenziosamente.
Ad esempio, ho avuto problemi a testare un evento di clic. Non importa quello che ho provato, semplicemente non chiamerebbe il suo ascoltatore. Poi ho capito che la funzione era stata derisa da Jest e non me l'ha mai detto.
Ma la peggiore offesa di Jest, di gran lunga, era che non aveva una modalità orologio per testare automaticamente le nuove modifiche. Potremmo eseguirlo una volta, ottenere i risultati dei test e il gioco è fatto. (Mi piace eseguire i miei test in background mentre lavoro. Altrimenti mi dimentico di eseguirli.) Oggi questo non è più un problema.
Oh, e Jest non supporta l'esecuzione di test React in più browser. Questo è un problema meno grave di prima, ma ritengo che sia una caratteristica importante per quella rara occasione in cui un heisenbug si verifica solo in una versione specifica di Chrome...
Nota del redattore: da quando questo articolo è stato originariamente scritto, Jest è notevolmente migliorato. Puoi leggere il nostro tutorial più recente, React Unit Testing Using Enzyme e Jest, e decidere tu stesso se il test Jest è all'altezza del compito al giorno d'oggi.
Test di reazione: un esempio integrato
Ad ogni modo, abbiamo visto come dovrebbe funzionare in teoria un buon test React front-end. Mettiamolo in pratica con un breve esempio.
Visualizzeremo diversi modi per generare numeri casuali utilizzando un componente grafico a dispersione realizzato con React e d3.js. Il codice e la sua demo sono anche su Github.
Useremo Karma come test runner, Mocha come framework di test e Webpack come caricatore di moduli.
Il set up
I nostri file sorgente andranno in una directory <root>/src
e inseriremo i test in una directory <root>/src/__tests__
. L'idea è che possiamo inserire diverse directory all'interno di src
, una per ogni componente principale e ciascuna con i propri file di test. Il raggruppamento di codice sorgente e file di test come questo semplifica il riutilizzo dei componenti di React in diversi progetti.

Con la struttura della directory in atto, possiamo installare dipendenze come questa:
$ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect
Se qualcosa non riesce a installare, prova a eseguire nuovamente quella parte dell'installazione. NPM a volte fallisce in modi che scompaiono con una nuova esecuzione.
Il nostro file package.json
dovrebbe assomigliare a questo quando abbiamo finito:
// 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" } }
Dopo alcune configurazioni, saremo in grado di eseguire test con npm test
o karma start
.
La configurazione
Non c'è molto nella configurazione. Dobbiamo assicurarci che Webpack sappia come trovare il nostro codice e che Karma sappia come eseguire i test.
Inseriamo due righe di JavaScript in un file ./tests.webpack.js
per aiutare Karma e Webpack a giocare insieme:
// tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);
Questo dice a Webpack di considerare qualsiasi cosa con un suffisso -test
come parte della suite di test.
La configurazione di Karma richiede un po' più di lavoro:
// 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 } }); };
La maggior parte di queste righe provengono da una configurazione Karma predefinita. Abbiamo utilizzato i browsers
per dire che i test devono essere eseguiti in Chrome, i frameworks
per specificare quale framework di test stiamo utilizzando e singleRun
per eseguire i test solo una volta per impostazione predefinita. Puoi mantenere il karma in esecuzione in background con karma start --no-single-run
.
Quei tre sono ovvi. La roba del Webpack è più interessante.
Poiché Webpack gestisce l'albero delle dipendenze del nostro codice, non è necessario specificare tutti i nostri files
. Abbiamo solo bisogno tests.webpack.js
, che quindi richiede tutti i file necessari.
Usiamo l'impostazione webpack
per dire a Webpack cosa fare. In un ambiente normale, questa parte andrebbe in un file webpack.config.js
.
Diciamo anche a Webpack di utilizzare il babel-loader
per i nostri JavaScript. Questo ci offre tutte le nuove fantastiche funzionalità di ECMAScript2015 e JSX di React.
Con la configurazione webpackServer
, diciamo a Webpack di non stampare alcuna informazione di debug. Rovinerebbe solo la nostra uscita di prova.
Un componente di reazione e un test
Con una suite di test in esecuzione, il resto è semplice. Dobbiamo creare un componente che accetti un array di coordinate casuali e crei un elemento <svg>
con un mucchio di punti.
Seguendo le migliori pratiche di test React, ovvero la pratica TDD standard, scriveremo prima il test, quindi il componente React effettivo. Iniziamo con un file di test vanilla 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()}; }); });
Per prima cosa abbiamo bisogno di React, i suoi TestUtils, expect
, la libreria Expect e il codice che stiamo testando. Quindi creiamo una nuova suite di test con describe
e creiamo alcuni dati casuali.
Per il nostro primo test, assicuriamoci che ScatterPlot
rendering di un titolo. Il nostro test va all'interno del blocco di 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"); });
La maggior parte dei test seguirà lo stesso schema:
- Rendi.
- Trova un nodo specifico.
- Controlla i contenuti.
Come abbiamo visto in precedenza, renderIntoDocument
il rendering del nostro componente, findRenderedDOMComponentWithTag
trova la parte specifica che stiamo testando e getDOMNode
ci fornisce l'accesso DOM grezzo.
All'inizio il nostro test fallirà. Per farlo passare, dobbiamo scrivere il componente che rende un tag title:
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;
Questo è tutto. Il componente ScatterPlot
il rendering di un <div>
con un tag <h1>
contenente il testo previsto e il nostro test passerà. Sì, è più lungo del semplice HTML, ma abbi pazienza.
Disegna il resto del gufo
Puoi vedere il resto del nostro esempio su GitHub, come menzionato sopra. Salteremo la descrizione passo passo in questo articolo, ma il processo generale è lo stesso di cui sopra. Voglio mostrarti un test più interessante, però. Un test che garantisce che tutti i punti dati vengano visualizzati sul grafico:
// 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); });
Come prima. Rendering, trovare nodi, controllare il risultato. La parte interessante qui è disegnare quei nodi DOM. Aggiungiamo un po' di d3.js magic al componente ScatterPlot
, in questo modo:
// 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)]); }, ...
Usiamo componentWillMount
per impostare scale d3 vuote per i domini X e Y e componentWillReceiveProps
per assicurarci che vengano aggiornati quando qualcosa cambia. Quindi update_d3
si assicura di impostare il domain
e l' range
per entrambe le scale.
Useremo le due scale per tradurre tra valori casuali nel nostro set di dati e posizioni sull'immagine. La maggior parte dei generatori casuali restituisce numeri nell'intervallo [0,1] , che è troppo piccolo per essere visto come pixel.
Quindi aggiungiamo i punti al metodo di rendering del nostro componente:
// 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> ); }
Questo codice passa attraverso l'array this.props.data
e aggiunge un elemento <circle>
per ogni datapoint. Semplice.
Se vuoi saperne di più sulla combinazione di React e d3.js per creare componenti di visualizzazione dei dati, questo è un altro ottimo motivo per consultare il mio libro, React+d3.js .
Test automatico dei componenti di reazione: più facile di quanto sembri
Questo è tutto ciò che dobbiamo sapere sulla scrittura di componenti front-end testabili con React. Per vedere altri test del codice dei componenti React, controlla la base di codice di esempio del test React su Github, come menzionato sopra.
Abbiamo imparato che:
- React ci costringe a modularizzare e incapsulare.
- Ciò rende i test dell'interfaccia utente di React facili da automatizzare.
- Gli unit test non sono sufficienti per i front-end.
- Karma è un ottimo corridore di prova.
- Jest ha del potenziale, ma non è ancora lì. (O forse ora lo è.)
Se ti è piaciuto questo articolo, seguimi su Twitter e lascia un commento qui sotto. Grazie per la lettura e buon test React!