Publié par

Il y a 5 ans -

Temps de lecture 9 minutes

Tester son application React

La complexité des applications web ayant augmenté en flèche ces dernières années, il est devenu impensable de ne plus tester unitairement son code JavaScript. React étant le nouveau challenger de choix pour le développement de frontend riche, la question de comment tester ses composants arrive naturellement assez rapidement. Et la réponse n’est malheureusement pas aussi simple qu’on pourrait s’y attendre : en effet, en plus des approches de tests déjà existantes, Facebook propose sa propre solution de test pour les applications React : Jest. Que vaut cette solution par rapport à un combo Karma / Mocha ? C’est ce que nous allons voir ci-dessous.

Jest

Jest est la solution proposée par Facebook pour tester les applications React. Jest est un framework de tests qui s’appuie sur Jasmine, et qui a comme particularité de tout mocker par défaut. Partant de l’hypothèse que lors d’un test unitaire il faut simuler le comportement de la majorité des dépendances du composant en cours de test, Jest a été créé pour que le fait de ne pas mocker soit l’exception et qu’il faille l’expliciter programmatiquement.

Autre particularité de Jest,  le framework s’appuie sur les conventions plus que la configuration pour fonctionner et permet donc de ne pas perdre plusieurs dizaines de minutes (voir d’heures) à le configurer. Déposez vos tests dans un répertoire __tests__ et ça fonctionne immédiatement.

Pour être plus explicite, nous allons prendre un exemple : imaginons que nous souhaitons tester le composant React suivant :

(Le code ci-dessous est directement issu de la documentation de Jest

/** @jsx React.DOM */

// fichier CheckboxWithLabel.js

var React = require('react/addons');
var CheckboxWithLabel = React.createClass({
  getInitialState: function() {
    return { isChecked: false };
  },
  onChange: function() {
    this.setState({isChecked: !this.state.isChecked});
  },
  render: function() {
    return (
      <label>
        <input
          type="checkbox"
          checked={this.state.isChecked}
          onChange={this.onChange}
        />
        {this.state.isChecked ? this.props.labelOn : this.props.labelOff}
      </label>
    );
  }
});

module.exports = CheckboxWithLabel;

Nous allons écrire le test Jest dans un répertoire __tests__ à la racine du projet :

/** @jsx React.DOM */

// fichier __tests__/CheckboxWithLabel.test.js

//On est obligé d'expliciter que le composant en cours de test n'est pas mocké
jest.dontMock('../CheckboxWithLabel.js');
describe('CheckboxWithLabel', function() {
  it('changes the text after click', function() {
    var React = require('react/addons');
    var CheckboxWithLabel = require('../CheckboxWithLabel.js');
    var TestUtils = React.addons.TestUtils;

    // Effectue le rendu du composant Checkbox dans le document
    var checkbox = TestUtils.renderIntoDocument(
      <CheckboxWithLabel labelOn="On" labelOff="Off" />
    );

    // Vérification que c'est 'Off' par défaut
    var label = TestUtils.findRenderedDOMComponentWithTag(
      checkbox, 'label');
    expect(label.getDOMNode().textContent).toEqual('Off');

    // Simule un click et vérifie que c'est maintenant 'On'
    var input = TestUtils.findRenderedDOMComponentWithTag(
      checkbox, 'input');
    TestUtils.Simulate.change(input);
    expect(label.getDOMNode().textContent).toEqual('On');
  });
});

Il reste maintenant un peu de configuration à faire pour installer Jest.

Tout d’abord il faut installer les dépendances nécessaires : Jest et un compilateur JSX, inclus dans React-tools

npm install --save-dev jest-cli react-tools

Il est ensuite nécessaire de créer un petit fichier preprocessor.js qui précompilera vos fichiers jsx pour les fournir à Jest :

// fichier preprocessor.js
var ReactTools = require('react-tools');
module.exports = {
  process: function(src) {
    return ReactTools.transform(src);
  }
};

Enfin, vous devez ajouter les lignes suivantes dans votre fichier package.json :

  "scripts": {
     "test": "jest"
  },
  "jest": {
    "scriptPreprocessor": "./preprocessor.js",
    "unmockedModulePathPatterns": ["./node_modules/react"]
  }

Il suffit alors de lancer la commande npm test pour que votre test s’exécute.

Cet exemple peut être retrouvé sur github.

Jest n’est cependant pas la seule solution pour pouvoir faire des tests avec React, et nous allons voir ci-dessous une approche plus traditionnelle utilisant Karma / Mocha / Chai.

Karma / Mocha / Chai

Là où Jest est un framework "tout en un", la stack que nous proposons ici est plutôt un ensemble d’outils fonctionnant bien ensemble. Nous avons choisi cette stack, car elle est la plus configurable, même si cela implique un coup d’entrée relativement élevé. Nous utiliserons Mocha, le framework de test à proprement parler, Chai, la librairie fournissant les assertions et enfin Karma, un test runner permettant d’exécuter les tests dans divers environnements suivant diverses configurations.
Cette stack est beaucoup plus classique, et elle peut être utilisée de la même façon avec d’autres technologies, telles que Angular ou Backbone. Nous nous concentrerons sur son utilisation dans le contexte d’un projet React, en prenant pour exemple le test du même composant que précédemment.

La première étape est d’installer toutes les dépendances nécessaires via npm :

npm install --save-dev karma-phantomjs-launcher karma-mocha karma-chai karma-cli karma-browserify reactify

Il faut ensuite configurer karma en créant un fichier karma.conf.js

module.exports = function (config) {
    config.set({
        basePath: '',
        frameworks: ['browserify', 'mocha', 'chai'],
        files: [
   //phantom JS ne possède pas la fonction bind utilisée massivement par React
           'functionBindPolyfill.js',
   //sur un vrai projet, on utiliserait ici des jokers * plutôt que les noms des fichiers
            'CheckboxWithLabel.js',
            'test/CheckboxWithLabel.test.js'
        ],
        preprocessors: {
            'CheckboxWithLabel.js': ['browserify'],
            'test/CheckboxWithLabel.test.js': ['browserify']
        },
        browserify: {
            debug: true,
            transform: [['reactify', {'es6': true, "strip-types": true}]],
            extensions: ['.js', '.jsx']
        },
        reporters: ['progress'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true,
        browsers: ['PhantomJS'],
        singleRun: false
    });
};

On peut ensuite ajouter dans le fichier package.json la commande pour lancer les tests :

"scripts": {
    "test": "karma start"
 }

Phantom JS, que nous utilisons ici, n’inclut pas la méthode bind sur le type Function. On a donc ici besoin d’un polyfill que l’on créera sous la forme du fichier functionBindPolyfill.js :

//PhantomJS ne supporte pas Function.bind, ci-dessous un polyfill
//https://github.com/ariya/phantomjs/issues/10522
function has(n){var t=featureMap[n];return isFunction(proto[t])}if(!Function.prototype.bind){var isFunction=function(n){return"function"==typeof n},bind,slice=[].slice,proto=Function.prototype,featureMap;featureMap={"function-bind":"bind"},has("function-bind")||(bind=function(n){var t=slice.call(arguments,1),i=this,o=function(){},e=function(){return i.apply(this instanceof o?this:n||{},t.concat(slice.call(arguments)))};return o.prototype=this.prototype||{},e.prototype=new o,e},proto.bind=bind)}

Ensuite écrire le test à proprement parler dans le répertoire test :

/** @jsx React.DOM */
 
// fichier test/CheckboxWithLabel.test.js
 
describe('CheckboxWithLabel', function() {
  it('changes the text after click', function() {
    var React = require('react/addons');
    var CheckboxWithLabel = require('../CheckboxWithLabel.js');
    var TestUtils = React.addons.TestUtils;
 
    // Effectue le rendu du composant Checkbox dans le document
    var checkbox = TestUtils.renderIntoDocument(
      <CheckboxWithLabel labelOn="On" labelOff="Off" />
    );
 
    // Vérification que c'est 'Off' par défaut
    var label = TestUtils.findRenderedDOMComponentWithTag(
      checkbox, 'label');
    expect(label.getDOMNode().textContent).to.be.equal('Off');
 
    // Simule un click et vérifie que c'est maintenant 'On'
    var input = TestUtils.findRenderedDOMComponentWithTag(
      checkbox, 'input');
    TestUtils.Simulate.change(input);
    expect(label.getDOMNode().textContent).to.be.equal('On');
  });
});

et enfin lancer le test par la commande npm test.

Comparaison entre les 2 approches

Le premier point que l’on peut constater, c’est que dans un cas simple comme notre exemple, les différences entre les 2 fichiers de tests sont mineures. Cependant, nous avons chez Xebia un projet avec des cas bien plus complexes et avons tester les 2 approches. On trouvera ci-dessous un résumé du comparatif que nous avons fait.

Bootstrapping

Comme on peut le constater dans les exemples ci-dessus, l’approche de Jest permet de démarrer des tests unitaires beaucoup plus rapidement. Même s’il y a malgré tout un peu de configuration, la simplicité de Jest l’emporte très largement sur Karma et Mocha, pour lesquels, même en connaissant bien les outils, il faut souvent plusieurs dizaines de minutes avant de pouvoir lancer son premier test.

Performance

Jest est plus lent que la stack Karma/Mocha, non seulement au premier lancement, mais surtout à l’usage en continu. En effet, Karma propose un mode watch permettant de relancer les tests automatiquement lorsqu’un fichier est modifié. Cela implique qu’on ne paye le coup de démarrage de Karma qu’une seule fois, là où avec Jest, il faut relancer l’ensemble du framework à chaque fois. À l’usage, Karma est ici beaucoup plus agréable d’utilisation.

Usage

Jest étant basé sur Jasmine, il est directement impacté par les choix de cette librairie : le système d’assertion embarqué est bien moins puissant que ce propose chai + sinon + chai-as-promised. Mais il a l’avantage d’être embarqué, ce qui évite d’avoir à télécharger tout internet pour pouvoir lancer ses tests. 

Mock

Le point fort de Jest est en fait assez discutable. Si dans une approche purement unitaire, il faudrait effectivement mocker toutes les dépendances, nous avons constaté que sur nos composants React, nous voulions souvent avoir des tests plus proches de l’intégration, en ne moquant pas les composants enfants ou le store dont dépend le composant. Au final, c’est sans doute un choix subjectif, mais nous trouvons préférable l’approche classique qui consiste à indiquer quelles sont les dépendances à mocker. Cependant, il faut reconnaître que Jest gagne sur l’aspect de la simplicité, car dans l’approche Karma / Mocha, il faut, pour moquer efficacement, utiliser des librairies dédiées. Nous utilisons pour notre part Sinon (librairie de mocking) et rewireify (permettant de moquer les require de browserify).

JsDOM vs Browser

Jest utilise JsDOM pour exécuter ses tests. Cela a théoriquement des avantages en terme de performance car il n’y a pas de navigateur réel à lancer mais nous avons constater qu’en réalité Jest était plus lent que Karma. 
L’approche Karma qui consiste à choisir explicitement sur quel navigateur lancer les tests nous semble préférable, d’autant plus qu’elle facilite le debugging en ayant accès aux consoles de développement des navigateurs.

Conclusion

Jest est le framework poussé par Facebook pour tester React, et nous nous étions donc précipité dessus. Nous nous sommes rapidement heurté à des murs, et c’est pourquoi nous avons finalement décidé de revenir à la stack Karma/Mocha/Chai. Cela n’enlève pas les qualités de Jest, mais nous pensons que ce framework est sans doute encore un peu trop jeune pour être utilisé. De nombreuses améliorations sont en cours pour corriger certains problèmes énumérés ci-dessus, et nous pouvons donc attendre avec impatience la prochaine version de Jest, qui peut être, sera un véritable challenger pour Karma/Mocha/Chai.

Publié par

Publié par Benoît Lemoine

Développeur et fier de l'être, Benoit s'intéresse de près à tout ce qui peut permettre de créer une application web, du HTML aux sources de données, en passant par le javascript et les framework haute productivité. twitter : @benoit_lemoine

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.