Testez vos composants React avec Enzyme

Les tests unitaires, c’est la vie.

On en fait tous (si si…), c’est tellement important que l’on imaginerait pas s’en passer. Faire sans, ce serait comme se lancer en pleine jungle avec une bouteille de vodka en guise de gourde, des tongs et un sac Hello Kitty.

Mais les tests unitaires avec ReactJS, de base, ce n’est pas simple. C’est là qu’intervient Enzyme, qui va faciliter les tests unitaires des composants React

Rapide démo

Les concepts

Enzyme propose trois grands concepts :

Mount

Pour effectuer le rendu du DOM et tester les méthodes du cycle de vie de votre application React (ComponentWill… ComponentDid…). Ca permet de tester le state, les props… ce qui fait la vie d’une application React en général.

Exemple depuis la doc :

import { mount } from 'enzyme';
import sinon from 'sinon';
import Foo from './Foo';

describe('<Foo />', () => {
  it('calls componentDidMount', () => {
    sinon.spy(Foo.prototype, 'componentDidMount');
    const wrapper = mount(<Foo />);
    expect(Foo.prototype.componentDidMount.calledOnce).to.equal(true);
  });
});

Shallow

Pour tester en isolation un composant React (sans état). On s’en sert essentiellement pour les dumbs components.

Exemple depuis la doc :

import { shallow } from 'enzyme';
import sinon from 'sinon';

describe('<MyComponent />', () => {
  it('should render three <Foo /> components', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper.find(Foo)).to.have.length(3);
  });

  it('should render an `.icon-star`', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper.find('.icon-star')).to.have.length(1);
  });
});

Render

Comme son nom l’indique, il s’agit de tester le rendu JSX / HTML statique de votre application.

import React from 'react';
import { render } from 'enzyme';
import PropTypes from 'prop-types';

describe('<Foo />', () => {
  it('renders three `.foo-bar`s', () => {
    const wrapper = render(<Foo />);
    expect(wrapper.find('.foo-bar')).to.have.length(3);
  });
});

Vous avez donc 3 apis qui couvrent la totalité des cas de tests dont vous pourriez avoir besoin pour écrire vos tests unitaires sur une application React.

On va créer une petite app React avec create-react-app, et mettre en place deux ou trois tests unitaires pour illustrer tout ça.

L'<App />

On commence par créer une nouvelle application avec create-react-app :

create-react-app enzyme

On va créer une petite ui simple sans fioritures pour illustrer les différents concepts. Disons que notre app affiche :

  • Un formulaire contenant un champ text et un bouton submit
  • Une liste d’éléments
  • Un bouton supprimer sur chaque élément

Oui ! Bien vu ! On fait une todo !

Gagnons du temps, je passe les détails de l’app qui est vraiment basique. Vous pouvez cloner le repo dispo à cette url :

https://github.com/GregoryBabonaux/enzymedemo

Pensez à virer les fichiers *.test.js si vous voulez écrire les tests from scratch.

Un petit yarn install puis un yarn start, et on est bons

La config

Maintenant qu’on a notre projet, on va installer ce dont on a besoin pour nos tests :

yarn add enzyme -dev

Si vous utilisez une version de react > 15.5 (ce qui devrait être le cas si vous utilisez create-react-app), pensez à ajouter ces dépendances supplémentaires :

yarn addreact-test-renderer react-dom 

On va également installer chai :

yarn add chai

Il vous suffira de lancer le test avec yarn pour lancer le bousin :

yarn test

Le vif du sujet

On a une app qui contient un container (App.js), nos composants (Todos), TodoForm et Todo)

<Todos /> et <Todo /> sont state less, ce sont des dumb components. On a besoin des refs avec <TodoForm /> pour gérer le champ text qui permet d’ajouter une todo. On gère tout le reste dans notre container <App />.

On va commencer par tester notre composant Todos. Créez un nouveau fichier Todos.test.js à la racine sur dossier src (au même niveau que Todos.js).

import React from 'react';
import { shallow, mount } from 'enzyme';
import { expect } from 'chai';

import Todos from './Todos';
import Todo from './Todo'

describe('<Todos />', () => {
 const todos = [
 {
  id: 1, 
  text: 'Manger'
 },
 {
  id: 2, 
  text: 'Boire'
 },
 {
  id: 3, 
  text: 'Dormir'
 },
 ]

const wrapper = shallow(<Todos todos={todos} />);

it('renders "Mes todos"', () => {
 const textHeader = <h2>Mes todos</h2>;
 expect(wrapper.contains(textHeader)).equal(true);
 });

it('should not renders just 1 todo item', () => {
 expect(wrapper.find(Todo)).to.not.have.length(1);
 })

it('renders 3 todo items', () => {
 expect(wrapper.find(Todo)).to.have.length(3);
 })
});

Un petit yarn test devrait vous donner ceci :

On va maintenant tester l’évènement onClick sur le bouton « supprimer » de Todo, on ajoute sinon pour gérer les évènements :

yarn add sinon

On créé un nouveau fichier Todo.test.js à la racine de src pour tester notre composant Todo :

import React from 'react';
import { shallow, mount } from 'enzyme';
import { expect } from 'chai';
import sinon from 'sinon';

import Todo from './Todo'

describe('<Todo />', () => {
 const todo = {
 id: 1, 
 text: 'Manger'
 }

it('simulate a click', () => {
 const onButtonClick = sinon.spy();
 const wrapper = shallow((
 <Todo onDeleteItem={onButtonClick} />
 ));
 wrapper.find('button').simulate('click');
 expect(onButtonClick).to.have.property('callCount', 1);
})

});
Ca passe toujours !

Si on écrivait un test unitaire pour notre <App /> ?

Il existe déjà un fichier de test par défaut dans notre projet, ca consiste à tester si le rendu de App se fait bien en montant le container dans le DOM avec ReactDOM. On va faire un peu mieux, on veut :

  • Tester si componentDidMount est bien appelé
  • Vérifier que notre state contient bien 3 todos
  • Vérifier au passage qu’on a bien un message de bienvenue
import React from 'react';
import ReactDOM from 'react-dom';

import sinon from 'sinon'
import { shallow, mount } from 'enzyme';
import { expect } from 'chai';

import App from './App';

describe('<App /> container', () => {

it('renders without crashing', () => {
 const div = document.createElement('div');
 ReactDOM.render(<App />, div);
 });

it('should display a welcome message', () => {
 const wrapper = shallow(<App />);
 const textHeader = <h2>Welcome to React</h2>;
 expect(wrapper.contains(textHeader)).equal(true);
 });

it('should call componentDidMount', () => {
 sinon.spy(App.prototype, 'componentDidMount');
 const wrapper = mount(<App />);
 expect(App.prototype.componentDidMount.calledOnce).to.equal(true);
 });

it('should have 3 todos in the state', () => {
 const wrapper = mount(<App />);
 expect(wrapper.state().todos).to.have.length(3)
 })

})
Ca passe toujours !

On teste actuellement notre composant <Todos /> pour savoir si 3 todos existe, mais on ne teste pas les props de ce composant. On va le mount pour s’assurer que tout est ok en modifiant Todos.test.js :

import React from 'react';
import { shallow, mount } from 'enzyme';
import { expect } from 'chai';

import Todos from './Todos';
import Todo from './Todo'

describe('<Todos />', () => {
 const todos = [
 {
 id: 1, 
 text: 'Manger'
 },
 {
 id: 2, 
 text: 'Boire'
 },
 {
 id: 3, 
 text: 'Dormir'
 },
 ]

const wrapper = shallow(<Todos todos={todos} />);

it('renders "Mes todos"', () => {
 const textHeader = <h2>Mes todos</h2>;
 expect(wrapper.contains(textHeader)).equal(true);
 });

it('should not renders just 1 todo item', () => {
 expect(wrapper.find(Todo)).to.not.have.length(1);
 })

it('renders 3 todo items', () => {
 expect(wrapper.find(Todo)).to.have.length(3);
 })

it('should have todos props', () => {
 const mountedWrapper = mount(<Todos todos={todos} />);
 expect(mountedWrapper.props().todos).to.have.length(3)
})

});

On va également améliorer notre test du container App en vérifiant qu’il contient bien un composant Todos et un composant TodoForm :

import React from 'react';
import ReactDOM from 'react-dom';

import sinon from 'sinon'
import { shallow, mount } from 'enzyme';
import { expect } from 'chai';

import App from './App';
import Todos from './Todos'
import TodoForm from './TodoForm'

describe('<App /> container', () => {

it('renders without crashing', () => {
 const div = document.createElement('div');
 ReactDOM.render(<App />, div);
 });

it('should display a welcome message', () => {
 const wrapper = shallow(<App />);
 const textHeader = <h2>Welcome to React</h2>;
 expect(wrapper.contains(textHeader)).equal(true);
 });

it('should call componentDidMount', () => {
 sinon.spy(App.prototype, 'componentDidMount');
 const wrapper = mount(<App />);
 expect(App.prototype.componentDidMount.calledOnce).to.equal(true);
 });

it('should have 3 todos in the state', () => {
 const wrapper = mount(<App />);
 expect(wrapper.state().todos).to.have.length(3)
 });

it('should contains a Todos component', () => {
 const wrapper = mount(<App />);
 expect(wrapper.find(Todos)).to.have.length(1)
 });

it('should contains a TodoForm component', () => {
 const wrapper = mount(<App />);
 expect(wrapper.find(TodoForm)).to.have.length(1)
 });

})
Toujours vert !

Et <TodoForm /> dans tout ça ?

Notre composant TodoForm contient un formulaire, qui contient un champ, qui a une ref, « todotext ». On s’en sert pour récupérer la valeur du champ, et réinitialiser le tout au submit.

On va vouloir tester que :

  • Le titre s’affiche bien (shallow)
  • Le submit du form fonctionne bien
import React from 'react';
import { shallow, mount } from 'enzyme';
import { expect } from 'chai';
import sinon from 'sinon';

import TodoForm from './TodoForm'

describe('<TodoForm />', () => {

it('should display a title', () => {
 const wrapper = shallow(<TodoForm />)
 const textHeader = <h2>Ajouter une todo</h2>;
 expect(wrapper.contains(textHeader)).equal(true);
 });

it('should submit event when click on submit', () => {
 const onSubmit = sinon.spy();
 const wrapper = mount(<TodoForm onSubmitForm={onSubmit} />);
 const button = wrapper.find('button');

button.simulate('submit');
 expect(onSubmit).to.have.property('callCount', 1);
 })

})

Ce serait tentant de tester la valeur du champ text avant et après le submit, mais – même si la frontière est parfois mince, on rentre alors plutôt dans les tests end to end.

Tout va bien

Voilà voilà

Bien entendu, tout ça reste une introduction. Vous pourriez améliorer ces tests (et je vous invite à le faire) en vous inspirant des différentes ressources sur le net.

Le plus difficile lorsque l’on écrit des tests unitaires, c’est de savoir quoi tester, et de ne pas rentrer dans le cerce vicieux de vouloir tout tester.

A vous de choisir votre voie.

 

Laisser un commentaire

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