Intermédiaire

React et les tests unitaires

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 je vais aborder dans cet article. Il sera question aujourd’hui de React.

Pour rappel, React est une bibliothèque qui permet de créer des composants réutilisable 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

La recommandation officielle : Jest

jestCommenç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 écrit 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.

{
  "babel": {
    "presets": [
      "es2015",
      "react"
    ]
  },
  "jest": {
    "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
    "unmockedModulePathPatterns": [
      "<rootDir>/node_modules/react",
      "<rootDir>/node_modules/react-dom",
      "<rootDir>/node_modules/react-addons-test-utils",
      "<rootDir>/node_modules/fbjs"
    ],
    "moduleFileExtensions": [
      "js",
      "jsx"
    ],
    "preprocessorIgnorePatterns": [
      "/node_modules/",
      "/fonts/",
      "/bootstrap/"
    ],
    "modulePathIgnorePatterns": [
      "/node_modules/"
    ],
    "cacheDirectory": "<rootDir>/jest-cache",
    "testRunner": "jasmine2",
    "testPathDirs": [
      "test/jest"
    ]
  }
}

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.

Ecrivons maintenant notre premier test :

'use strict';
jest.unmock('../../../src/views/product');

import React from 'react';
import {
  renderIntoDocument,
  findRenderedDOMComponentWithTag
} from 'react-addons-test-utils';
import Product from '../../../src/views/product';

describe('Product', () => {
  it('Should render product with name, price and stock', () => {
    const testedComponent = renderIntoDocument(
      <Product
        product={{}}
        withStock={false}
      />
    );
    const productTitle = findRenderedDOMComponentWithTag(testedComponent, 'h3');
    expect(productTitle.textContent).toEqual('');
  });
});

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 test à proprement parlé.

A l’exécution cela donne :

flux-react@1.0.0 test-jest C:\Users\Pioupiou\WebstormProjects\flux-react
jest --no-cache

Using Jest CLI v14.1.0, jasmine2, babel-jest
 PASS  test\jest\__tests__\product.test.js (0.648s)
1 test passed (1 total in 1 test suite, run time 1.276s)

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 à 3s pour le test et supérieur à 5s 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 raffraichit 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 sont disponibles sur le blog de Jest.

Une alternative intéressante avec l’ensemble Mocha/Chai/Jsdom/Enzyme

mochaEssayons à présent une alternative à Jest.
Le monde javascript étant largement plus ancien que React, il y a de nombreux outils déjà existant 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 librairie d’assertion 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
– Eventuellement une bibliothèque de mock sinon
– En bonus, Une bibliothèque de manipulation de composant React : Enzyme

Enzyme mis à part, aucun de ces outils n’est propre à l’écosystème React. Le premier avantage direct concernent les retours d’expérience et la communauté. Le second avantage est qu’il n’y à qu’une ligne de commande a paramétré 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 fournis par les navigateurs web.

import jsdom from 'jsdom';

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

global.document = jsdom.jsdom('');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
  if (typeof global[property] === 'undefined') {
    exposedProperties.push(property);
    global[property] = document.defaultView[property];
  }
});

global.navigator = {
  userAgent: 'node.js',
};

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

'use strict';
import React from 'react';
import Product from '../../src/views/product';

import { expect } from 'chai';
import { shallow } from 'enzyme';

describe('Product', () => {
  it('Should render product with name, price and stock', () => {
    const product = shallow(
        <Product
            product={{}}
            withStock={false}
        />
    );
    console.log(product.debug());
    const productTitle = product.find('h3');
    expect(productTitle.text()).to.be.empty;
  });
});

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 moindre, 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 :

<A>
        <h1></h1>
        <div>
            <B>
                <h2></h2>
                <div></div>
                <C>
                    <h3></h3>
                    <div></div>
                </C>
            </B>
        </div>
</A>

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

<A>
        <h1></h1>
        <div>
            <B>
            </B>
        </div>
</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 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 tiers (à limiter le plus possible), on pourrait utiliser Sinonjs.

Dans notre exemple, Enzyme fourni 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 callback de lifecycle des composants React.
– mount() : render du composant React en le montant réellement dans le DOM. Cela déclenche les callback 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 coeur 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 rendu 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 fournis par Enzyme n’est pas claire, 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) :


flux-react@1.0.0 test-enzyme C:\Users\Pioupiou\WebstormProjects\flux-react
mocha --compilers js:babel-core/register --require ./test/jsdom-setup.js "test/enzyme/**/*.@(js|jsx)"



  Product
<div className="product col-lg-10">
<h3 className="col-lg-12" />
<div className="col-lg-6 price" />
<div className="col-lg-6 qty" />
</div>
    √ Should render product with name, price and stock


  1 passing (14ms)


Process finished with exit code 0

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

En 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’enrichi release après release de features intéressantes. Il peut se revéler salutaire si l’on veut tester un code legacy éventuellement mal découpé grâce à son système de mock automatique.

Mocha/chai & jsDom sont les outils qui à mon sens sont les plus fourni et reconnu 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’affut des nouveautés qui pourraient compléter efficacement notre boite à outil 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 terme d’affichage/logique metier. 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.

© SOAT
Toute reproduction interdite sans autorisation de la société SOAT

Nombre de vue : 1011

AJOUTER UN COMMENTAIRE