Sviluppo React.js basato su test: test unitario React.js con Enzima e Jest

Pubblicato: 2022-03-11

Qualsiasi pezzo di codice che non ha test è detto codice legacy, secondo Michael Feathers. Pertanto, uno dei modi migliori per evitare la creazione di codice legacy è l'utilizzo dello sviluppo basato su test (TDD).

Sebbene siano disponibili molti strumenti per il test unitario di JavaScript e React.js, in questo post utilizzeremo Jest ed Enzyme per creare un componente React.js con funzionalità di base utilizzando TDD.

Perché utilizzare TDD per creare un componente React.js?

TDD apporta molti vantaggi al tuo codice: uno dei vantaggi dell'elevata copertura dei test è che consente un facile refactoring del codice mantenendo il tuo codice pulito e funzionale.

Se hai già creato un componente React.js, ti sei reso conto che il codice può crescere molto velocemente. Si riempie di molte condizioni complesse causate da dichiarazioni relative a cambiamenti di stato e chiamate di servizio.

Ogni componente privo di unit test ha codice legacy che diventa difficile da mantenere. Potremmo aggiungere unit test dopo aver creato il codice di produzione. Tuttavia, potremmo correre il rischio di trascurare alcuni scenari che avrebbero dovuto essere testati. Creando prima i test, abbiamo maggiori possibilità di coprire ogni scenario logico nel nostro componente, il che semplificherebbe il refactoring e la manutenzione.

Come testiamo un componente React.js?

Ci sono molte strategie che possiamo usare per testare un componente React.js:

  • Possiamo verificare che una particolare funzione negli props di scena sia stata chiamata quando viene inviato un determinato evento.
  • Possiamo anche ottenere il risultato della funzione di render dato lo stato del componente corrente e abbinarlo a un layout predefinito.
  • Possiamo anche verificare se il numero dei figli del componente corrisponde a una quantità prevista.

Per utilizzare queste strategie, utilizzeremo due strumenti utili per lavorare con i test in React.js: Jest ed Enzyme.

Utilizzo di Jest per creare unit test

Jest è un framework di test open source creato da Facebook che ha un'ottima integrazione con React.js. Include uno strumento da riga di comando per l'esecuzione dei test simile a quello offerto da Jasmine e Mocha. Ci consente anche di creare funzioni fittizie con una configurazione quasi nulla e fornisce un set di abbinamenti davvero carino che rende le asserzioni più facili da leggere.

Inoltre, offre una funzionalità davvero interessante chiamata "test snapshot", che ci aiuta a controllare e verificare il risultato del rendering dei componenti. Useremo il test delle istantanee per catturare l'albero di un componente e salvarlo in un file che possiamo usare per confrontarlo con un albero di rendering (o qualunque cosa passiamo alla funzione expect come primo argomento).

Utilizzo dell'enzima per montare i componenti di React.js

Enzyme fornisce un meccanismo per montare e attraversare gli alberi dei componenti di React.js. Questo ci aiuterà ad ottenere l'accesso alle sue proprietà e al suo stato, nonché ai suoi oggetti di scena figli per eseguire le nostre asserzioni.

Enzyme offre due funzioni di base per il montaggio dei componenti: shallow e mount . La funzione shallow carica in memoria solo il componente radice mentre mount carica l'intero albero DOM.

Uniremo Enzyme e Jest per montare un componente React.js ed eseguire asserzioni su di esso.

Passaggi TDD per creare un componente di reazione

Configurare il nostro ambiente

Puoi dare un'occhiata a questo repository, che ha la configurazione di base per eseguire questo esempio.

Stiamo usando le seguenti versioni:

 { "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }

Creazione del componente React.js utilizzando TDD

Il primo passaggio consiste nel creare un test non riuscito che proverà a eseguire il rendering di un componente React.js utilizzando la funzione superficiale dell'enzima.

 // 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 />); }); });

Dopo aver eseguito il test, otteniamo il seguente errore:

 ReferenceError: MyComponent is not defined.

Creiamo quindi il componente che fornisce la sintassi di base per superare il test.

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }

Nel passaggio successivo, ci assicureremo che il nostro componente esegua il rendering di un layout dell'interfaccia utente predefinito utilizzando la funzione toMatchSnapshot di Jest.

Dopo aver chiamato questo metodo, Jest crea automaticamente un file snapshot chiamato [testFileName].snap , che viene aggiunto alla cartella __snapshots__ .

Questo file rappresenta il layout dell'interfaccia utente che ci aspettiamo dal rendering dei nostri componenti.

Tuttavia, dato che stiamo cercando di eseguire il TDD puro , dovremmo prima creare questo file e quindi chiamare la funzione toMatchSnapshot per far fallire il test.

Questo può sembrare un po' confuso, dato che non sappiamo quale formato usa Jest per rappresentare questo layout.

Potresti essere tentato di eseguire prima la funzione toMatchSnapshot e vedere il risultato nel file snapshot, e questa è un'opzione valida. Tuttavia, se vogliamo veramente utilizzare il TDD puro , dobbiamo imparare come sono strutturati i file snapshot.

Il file snapshot contiene un layout che corrisponde al nome del test. Ciò significa che se il nostro test ha questa forma:

 desc("ComponentA" () => { it("should do something", () => { … } });

Dovremmo specificarlo nella sezione delle esportazioni: Component A should do something 1 .

Puoi leggere di più sul test degli snapshot qui.

Quindi, creiamo prima il file MyComponent.test.js.snap .

 //__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;

Quindi, creiamo lo unit test che verificherà che lo snapshot corrisponda agli elementi figlio del componente.

 // MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...

Possiamo considerare components.getElements come il risultato del metodo di rendering.

Passiamo questi elementi al metodo expect per eseguire la verifica rispetto al file snapshot.

Dopo aver eseguito il test otteniamo il seguente errore:

 Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []

Jest ci dice che il risultato di component.getElements non corrisponde allo snapshot. Quindi, facciamo passare questo test aggiungendo l'elemento di input in MyComponent .

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }

Il passaggio successivo consiste nell'aggiungere funzionalità input eseguendo una funzione quando il suo valore cambia. Lo facciamo specificando una funzione nella prop onChange .

Per prima cosa dobbiamo modificare lo snapshot per far fallire il test.

 //__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;

Uno svantaggio della prima modifica dell'istantanea è che l'ordine degli oggetti di scena (o attributi) è importante.

Jest ordinerà in ordine alfabetico gli oggetti di scena ricevuti nella funzione expect prima di verificarli rispetto allo snapshot. Quindi, dovremmo specificarli in quest'ordine.

Dopo aver eseguito il test otteniamo il seguente errore:

 Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]

Per superare questo test, possiamo semplicemente fornire una funzione vuota a onChange .

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }

Quindi, ci assicuriamo che lo stato del componente cambi dopo l'invio dell'evento onChange .

Per fare ciò, creiamo un nuovo unit test che chiamerà la funzione onChange nell'input passando un evento per simulare un evento reale nell'interfaccia utente.

Quindi, verifichiamo che lo stato del componente contenga una chiave denominata input .

 // 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(); });

Ora otteniamo il seguente errore.

 Expected value to be defined, instead received undefined

Ciò indica che il componente non ha una proprietà nello stato chiamato input .

Facciamo il test superato impostando questa voce nello stato del componente.

 // 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>; } }

Quindi, dobbiamo assicurarci che sia impostato un valore nella nuova voce di stato. Otterremo questo valore dall'evento.

Quindi, creiamo un test che assicuri che lo stato contenga questo valore.

 // 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: ""

Infine facciamo passare questo test ottenendo il valore dall'evento e impostandolo come valore di input.

 // 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>; } }

Dopo aver verificato che tutti i test siano stati superati, possiamo eseguire il refactoring del nostro codice.

Possiamo estrarre la funzione passata nel prop onChange in una nuova funzione chiamata 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>; } }

Ora abbiamo un semplice componente React.js creato usando TDD.

Sommario

In questo esempio, abbiamo provato a utilizzare il TDD puro seguendo ogni passaggio scrivendo il minor numero di codice possibile per non riuscire e superare i test.

Alcuni passaggi potrebbero sembrare non necessari e potremmo essere tentati di saltarli. Tuttavia, ogni volta che saltiamo qualsiasi passaggio, finiremo per utilizzare una versione meno pura di TDD.

Anche l'utilizzo di un processo TDD meno rigoroso è valido e potrebbe funzionare perfettamente.

Il mio consiglio per te è di evitare di saltare qualsiasi passaggio e di non sentirti male se lo trovi difficile. Il TDD è una tecnica non facile da padroneggiare, ma vale sicuramente la pena farlo.

Se sei interessato a saperne di più sul TDD e sul relativo sviluppo guidato dal comportamento (BDD), leggi Your Boss Won't Appreciate TDD del collega Toptaler Ryan Wilcox.