Tutoriel pour apprendre à écrire des tests unitaires pour des applications React

Nordwin HOFF (Soat)

Lorsqu'on choisit une technologie orientée composant pour son développement d'applications front end, on se donne la possibilité de construire son application à l'aide de briques faiblement couplées les unes aux autres. Cela constitue un avantage pour la réutilisation du code ainsi que pour les tests. C'est cet aspect que je vais aborder dans ce tutoriel. Il sera question aujourd'hui de React.

Commentez

- Titre : Tutoriel pour apprendre à écrire des tests unitaires pour des applications React

- Auteur : Nordwin HOFF (Soat)

- Parution : 9 février 2017

- Licence : Copyright ® 2016 Nordwin HOFF. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. La recommandation officielle : Jest

- III. Une autre solution intéressante avec l'ensemble Mocha/Chai/Jsdom/Enzyme

- IV. Conclusion

- V. Remerciements

I - Introduction

Pour rappel, React est une bibliothèque qui permet de créer des composants réutilisables pour construire des interfaces graphiques. Elle peut être utilisée côté client ou côté serveur ce qui en fait un outil de JavaScript isomorphique. Ce n'est pas un framework. Il n'y a pas de paradigme attenant à la technologie elle-même.

Ce rappel étant fait, il faut signaler que la bonne pratique en termes d'organisation de code pour son application React est la mise en place du pattern Flux recommandé par Facebook. L'idéal selon moi, est l'utilisation de Redux qui va amener une organisation du code qui découple complètement la partie affichage des données de la partie code métier dans les reducers.

Lors d'un précédent article, j'ai présenté le pattern Flux. Je vais partir des composants créés pour l'occasion et me focaliser sur les tests unitaires. L'idée étant de faire un tour d'horizon des technologies existantes au travers d'un test simple.

Vous trouverez le code lié a cet article dans son intégralité sur ce repository github : https://github.com/SoatGroup/flux-react

II - La recommandation officielle : Jest

Commençons notre revue par l'outil officiel préconisé et mis à disposition par Facebook : Jest.

Jest est un framework de tests unitaires conçu pour React avec un parti pris, celui de mocker toutes les dépendances par défaut.
Il fournit un DOM pour une exécution des tests sans lancer de navigateur. Il est construit au-dessus de Jasmine, c'est donc cette API qu'on utilisera pour l'écriture des tests et assertions.

En plus de Jest, React propose un ensemble de fonctions utiles à la manipulation des composants dans le cadre de tests au travers de l'API react-addons-test-utils.

Avant d'écrire notre premier test unitaire avec Jest, il nous faut installer les dépendances nécessaires aux tests écrits de préférence en ES2015.
Nous avons besoin de : babel-jest, babel-preset-es2015, babel-preset-react et jest-cli. Celles-ci sont présentes dans le fichier package.json sur Github.

Nous avons également besoin d'un peu de configuration. Celle-ci prend place dans le fichier package.json. On y ajoute la configuration de Jest et de Babel qui permet la traduction du code ES2015 dans le contexte Jest.

1. {

2. "babel": {

3. "presets": [

4. "es2015",

5. "react"

6. ]

7. },

8. "jest": {

9. "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",

10. "unmockedModulePathPatterns": [

11. "<rootDir>/node_modules/react",

12. "<rootDir>/node_modules/react-dom",

13. "<rootDir>/node_modules/react-addons-test-utils",

14. "<rootDir>/node_modules/fbjs"

15. ],

16. "moduleFileExtensions": [

17. "js",

18. "jsx"

19. ],

20. "preprocessorIgnorePatterns": [

21. "/node_modules/",

22. "/fonts/",

23. "/bootstrap/"

24. ],

25. "modulePathIgnorePatterns": [

26. "/node_modules/"

27. ],

28. "cacheDirectory": "<rootDir>/jest-cache",

29. "testRunner": "jasmine2",

30. "testPathDirs": [

31. "test/jest"

32. ]

33. }

34. }

La configuration Babel est la plus simple, on précise juste les presets utilisées.
Concernant Jest, on précise :

- l'utilisation de babel-jest avant l'exécution des tests eux-mêmes ;

- les modules qu'on ne veut pas mocker par défaut ;

- les modules à ne pas examiner au préprocess ;

- la configuration supplémentaire pour son projet. Ici, les tests sont dans le dossier test/jest, on désactive le cache et on force le testrunner à jasmine2.

On pourrait ajouter d'autres paramétrages comme le nom du dossier contenant les tests. Il faut savoir que par défaut, Jest cherche les tests dans un dossier nommé __tests. La configuration ci-dessus indique à Jest de chercher tous les tests dans le dossier configuré test/jest.

La liste exhaustive des paramétrages possibles et valeurs par défaut se trouve ici.

Écrivons maintenant notre premier test :

1. 'use strict';

2. jest.unmock('../../../src/views/product');

4. import React from 'react';

5. import {

6. renderIntoDocument,

7. findRenderedDOMComponentWithTag

8. } from 'react-addons-test-utils';

9. import Product from '../../../src/views/product';

11. describe('Product', () => {

12. it('Should render product with name, price and stock', () => {

13. const testedComponent = renderIntoDocument(

14. <Product

15. product={{}}

16. withStock={false}

17. />

18. );

19. const productTitle = findRenderedDOMComponentWithTag(testedComponent, 'h3');

20. expect(productTitle.textContent).toEqual('');

21. });

22. });

Comme expliqué en introduction, ce test très basique est là pour illustrer l'utilisation de Jest. Ici, je teste que dans le rendu à vide du composant, j'ai bien un titre (H3) vide.
Dans les premiers blocs de code, on voit qu'il est nécessaire d'indiquer à Jest de ne pas mocker le composant que l'on veut tester. Ensuite, on importe classiquement toutes les classes et méthodes que l'on va utiliser et enfin on le teste à proprement parler.

À l'exécution cela donne :

1. flux-react@1.0.0 test-jest C:\Users\Pioupiou\WebstormProjects\flux-react

2. jest --no-cache

4. Using Jest CLI v14.1.0, jasmine2, babel-jest

5. PASS  test\jest\__tests__\product.test.js (0.648s)

6. 1 test passed (1 total in 1 test suite, run time 1.276s)

8. Process finished with exit code 0

Le test est passant (ouf !). Le temps d'exécution est la première chose qui m'a frappé lorsque j'ai lancé un test pour la première fois avec Jest. Il était supérieur à 3 s pour le test et supérieur à 5 s pour l'ensemble (bootstrap, jest, etc.).
Malgré un temps encore assez important pour un test aussi simple, les performances sont en constante amélioration au fil des nouvelles releases.
Je conseillerais également la désactivation du cache pendant l'écriture des tests. En effet ce dernier ne se rafraichit pas de manière aussi fiable que l'on pourrait attendre.

Malgré une première expérience mitigée avec Jest, il faut noter que Facebook fait évoluer régulièrement son outil pour le rendre plus facile d'utilisation. La liste des nouveautés est disponible sur le blog de Jest.

III - Une autre solution intéressante avec l'ensemble Mocha/Chai/Jsdom/Enzyme

Essayons à présent un substitut à Jest.
Le monde JavaScript étant largement plus ancien que React, il y a de nombreux outils déjà existants pour tester son code. Les développeurs familiers de nodeJS connaissent l'ensemble mocha/chai pour la mise en place de tests unitaires. Pourquoi donc ne pas tester avec ces outils nos composants React ?
Jest nous apporte d'emblée, un test runner, une bibliothèque d'assertions et un dom. La difficulté ici est donc de devoir chercher chaque outil indépendamment. Dans notre cas, il nous faut donc :

- un test runner : Mocha ;

- une bibliothèque d'assertion : Chai ;

- un dom : jsDom ;

- éventuellement une bibliothèque de mock sinon ;

- en bonus, une bibliothèque de manipulation de composants React : Enzyme.

Enzyme mis à part, aucun de ces outils n'est propre à l'écosystème React. Le premier avantage direct concerne les retours d'expérience et la communauté. Le second avantage est qu'il n'y a qu'une ligne de commande à paramétrer et un jsdom à initialiser et c'est parti !

En amenant l'utilisation de Enzyme, on bénéficie d'une documentation très complète, il y a notamment un fichier de paramétrage de jsdom prêt à l'emploi. Dans notre cas, on veut qu'il soit le plus proche possible du DOM fourni par les navigateurs web.

1. import jsdom from 'jsdom';

3. const exposedProperties = ['window', 'navigator', 'document'];

5. global.document = jsdom.jsdom('');

6. global.window = document.defaultView;

7. Object.keys(document.defaultView).forEach((property) => {

8. if (typeof global[property] === 'undefined') {

9. exposedProperties.push(property);

10. global[property] = document.defaultView[property];

11. }

12. });

14. global.navigator = {

15. userAgent: 'node.js',

16. };

En changeant d'outil, on change d'API de test :

1. 'use strict';

2. import React from 'react';

3. import Product from '../../src/views/product';

5. import { expect } from 'chai';

6. import { shallow } from 'enzyme';

8. describe('Product', () => {

9. it('Should render product with name, price and stock', () => {

10. const product = shallow(

11. <Product

12. product={{}}

13. withStock={false}

14. />

15. );

16. console.log(product.debug());

17. const productTitle = product.find('h3');

18. expect(productTitle.text()).to.be.empty;

19. });

20. });

Dans ce test, on voit que la déclaration des tests avec Mocha (describe(), it()) est identique à celle de Jasmine.
En revanche, en utilisant Chai, on peut utiliser une syntaxe que personnellement, je préfère à celle de Jasmine (le fameux to.be.empty dans notre cas).

Un autre changement et pas des moindres, c'est l'API Enzyme. Celle-ci propose des méthodes au nom bien plus court que l'addon de test de react et surtout une API bien plus fournie. L'idée d'Enzyme et plus largement des tests de composants Front est de faire du shallow rendering. Cette technique consiste à ne rendre les composants que sur le premier niveau d'imbrication.

Concrètement, prenons un composant C dans un composant B lui-même contenu dans un composant A. Chacun contient une div et titre. Si je veux afficher ces composants dans le DOM j'obtiens :

1. <A>

2. <h1></h1>

3. <div>

4. <B>

5. <h2></h2>

6. <div></div>

7. <C>

8. <h3></h3>

9. <div></div>

10. </C>

11. </B>

12. </div>

13. </A>

Si avec Enzyme, je fais un shallow(A) j'obtiens:

1. <A>

2. <h1></h1>

3. <div>

4. <B>

5. </B>

6. </div>

7. </A>

Cela rend le test vraiment unitaire d'un point de vue du composant, car on se préoccupe uniquement de ce qui se passe dans A. S'il y a des dépendances vers les enfants, on vérifiera les props envoyées aux enfants, mais pas ce qui en est fait au niveau du dessous.
Dans notre exemple, la conséquence est qu'on n'a pas besoin d'importer ou de mocker B. B est implicitement mocké par le shallow rendering.
Dans le cas où nous aurions dans A des dépendances externes à des bibliothèques tierces (à limiter le plus possible), on pourrait utiliser Sinonjs.

Dans notre exemple, Enzyme fournit des méthodes pour jouer sur les différents niveaux de rendu :

- shallow() : render du composant React selon le principe du shallow rendering. Cela n'exécute pas les colbacks de lifecycle des composants React ;

- mount() : render du composant React en le montant réellement dans le DOM. Cela déclenche les colbacks de lifecycle des composants React ;

- render() : render static du composant dans le DOM.

Le gros avantage c'est qu'Enzyme ne renvoie pas un DomElement comme les react-addons-test-utils, mais un wrapper sur le composant rendu. Pour les personnes qui, comme moi, ne connaissent pas par cœur l'API DOM standard, c'est un gain de temps inestimable.

C'est au travers du Wrapper que la manipulation de composant React est facilitée. L'API complète est décrite dans la documentation Enzyme. Ici, on l'utilise pour retrouver nos Tag H3 et vérifier son contenu.

Autre avantage, on peut visualiser facilement dans la console, les composants rendus dans le test au travers de la méthode debug(). J'ai ajouté ici console.log(product.debug()).
Si le shadow rendering et la différence entre les méthodes fournies par Enzyme ne sont pas clairs, je recommande de tester les différents résultats avec la méthode debug().

Après cette description rapide d'Enzyme, il est temps de lancer notre test. On ajoute à notre script npm la commande suivante :

mocha --compilers js:babel-core/register --require ./test/jsdom-setup.js \"test/enzyme/**/*.@(js|jsx)\"

On obtient (avec le debug du composant) :

1. flux-react@1.0.0 test-enzyme C:\Users\Pioupiou\WebstormProjects\flux-react

2. mocha --compilers js:babel-core/register --require ./test/jsdom-setup.js "test/enzyme/**/*.@(js|jsx)"

6. Product

7. <div className="product col-lg-10">

8. <h3 className="col-lg-12" />

9. <div className="col-lg-6 price" />

10. <div className="col-lg-6 qty" />

11. </div>

12. √ Should render product with name, price and stock

15. 1 passing (14ms)

18. Process finished with exit code 0

Le test est passant là aussi (re-ouf !). Le temps d'exécution du test est de 14 ms soit 634 ms de moins qu'avec Jest. Cela n'a pas valeur de benchmark, mais comme premier ressenti, cela fait forte impression.

IV - Conclusion

Après ce premier aperçu des possibilités de tests unitaires, on se rend compte qu'il n'y a pas qu'une seule façon de tester son application React.

Jest est jeune et s'enrichit release après release de features intéressantes. Il peut se révéler salutaire si l'on veut tester un code legacy éventuellement mal découpé grâce à son système de mock automatique.

Mocha/chai et jsDom sont les outils qui à mon sens sont les plus fournis et reconnus dans le monde du test JavaScript.

Enfin, Enzyme est à mes yeux la bibliothèque incontournable que tout projet React devrait utiliser pour la mise en place de tests unitaires de composants.

Le monde JavaScript évoluant très rapidement, je recommande de tester régulièrement les nouvelles versions de chaque outil et de se tenir à l'affût des nouveautés qui pourraient compléter efficacement notre boite à outils du monde JavaScript.

Avant de se quitter, mon côté craftsman se sent obligé de rappeler qu'une application React bien construite doit avoir un bon découpage en termes d'affichage/logique métier. Dans cet esprit, le test des composants React ne devrait pas être le plus important en termes de volumétrie.

J'encourage donc l'utilisation de pattern de type Redux.

Pour rappel, le code lié à cet article est disponible dans son intégralité sur le GitHub de Soat.

V - Remerciements

Nous tenons à remercier la société Soat qui nous a autorisés à publier ce tutoriel.

Nous remercions également Winjerome pour la mise au gabarit et Claude Leloup pour la relecture orthographique.