Composant React : introduction

React.js c’est la bibliothèque graphique crée par Facebook pour la création de front ends web interactives. Le composant React met en avance une approche basée sur des composants : une application est un ensemble de composants imbriqués qui React se charge de transformer en page web.
Dans cet article on parlera de comment écrire des tests (unitaires, mais pas que) pour des composants React. Pourquoi? Car un composant React, c’est du code Javascript. Un composant peut embarquer de la logique assez complexe. On écrit des tests pour s’assurer que notre code se comporte comme prévu, pour s’assurer que des modifications futures ne cassent pas des comportements vitaux, et pour éviter des régressions.

Avant de commencer: il y a quoi à tester?

Concrètement, un composant React est défini soit comme une fonction soit comme une classe. Un composant a un ensemble de propriétés (props) et un état local (state). Un composant embarque du code Javascript qui génère à partir de son état et de ses props un arbre d’éléments React. Les nœuds représentent des composants React et/ou des balises HTML (appelé DOM virtuel).
React se charge de mettre à jour le DOM de la page dès que les props ou le state d’un composant changent en comparant le DOM virtuel et le DOM réel. De ce fait, seul le sous ensemble de la page web qui a changé se met à jours. Ces concepts son illustrés dans la figure suivante.

Dans le cas où le composant est défini comme une fonction, un composant est une fonction qui prends en entrée des props et retourne un arbre de Il est donc stateless.

Dans l’exemple ci-dessous, on définit un composant appelé Badge avec la propriété (props) name. Une fois exécutée, la fonction retourne le rendu du composant. Le rendu du composant est composé d’une balise div avec le texte Hello {name}, this is your badge! à l’intérieur. La syntaxe {name} est utilisé pour afficher la valeur de la propriété name dans le code HTML du rendu du composant.

const Badge = ({name}) => (
    <div>
        Hello {name}, this is your badge!
    </div>
);

Un composant peut aussi être décrit comme une class Javascript. Dans ce cas, on pourra définir et manipulé l’état du composant. Où pourra aussi réagir à des changements d’état du composant dans son cycle de vie.

La figure suivante illustre le cycle de vie d’un composant React :

Sans expliquer en détails chaque étape du cycle de vie, en tant que développeur on peut écrire du code pour se brancher sur les étapes :

  • D’initialisation: de l’état EntryPoint à l’état Rendered;
  • De la décision du déclenchement d’une régénération du rendu graphique d’un composant en function des changements de props/états: les états PropsChanged et StateChanged;
  • De la régénération effective du rendu du composant: de l’état ShouldUpdate à l’état Rendered.

Ci-dessous, on montre comment décrire un composant React en tant que classe Javascript. De plus, on montre comment surcharger son comportement juste avant d’être monté sur le DOM.

class BetterBadge extends React.Component {
    componentWillMount() {
        console.log('Do something when component mounts');
    }

    render() {
        return (
            <div>
                Hello {this.props.name}, this is your badge!
            </div>
        );
    }
}

La plupart du temps, voici ce qu’on va vouloir tester chez un component React:

  • Étant donnée l’état de ses props/state, quel arbre de composants un composant doit générer?
  • Comment se comporte le composant dans les différentes étapes de son cycle de vie?
  • Comment se comporte l’arbre de composants face à des événements UI?

Le but de cet article n’est pas de présenter une liste exhaustive des outils existants de test de composants React, mais de présenter les méthodes les plus utilisés. En général, un teste d’un composant React se fait en trois étapes: d’abord on crée une instance du composant, après on utilise l’instance pour générer un arbre DOM (virtuel ou réel) sur lequel on pourra écrire des asserts pour vérifier ses propriétés attendues.

L’objet qui prends en entrée une instance de composant React et génère un rendu DOM est un renderer. Dans cet article on verra les renderers disponibles dans l’API standard React pour tester des components.

Dans la section Pour aller plus loin vous trouverez des liens vers des articles plus complets, et d’autres APIs.

Mon composant React génère-t-il le bon arbre de sous éléments?

Pour tester ça, le plus simple c’est d’utiliser ce qu’on appelle le shallow rendering. Il s’agit de générer l’arbre du DOM virtuel du composant, mais en s’arrêtant au premier niveau d’imbrication (i.e. sans générer le rendu des sous composants). Cela nos donne un moyen pas cher de tester unitairement un composant.

Imaginons le composant suivant qui réutilise le composant Badge précédemment défini.

const UserProfile = ({userFirstName, userFamilyName, userLogin}) => (
    <div>
        <Badge name={userFirstName}/>
        <p>User login: {userLogin}</p>
        <p>User name: {userFamilyName.toUpperCase()} {userFirstName}</p>
    </div>
);

Ce composant affiche directement le prénom et nom de l’utilisateur (dans des balises HTML p) mais réutilise le composant Badge pour afficher un message de bienvenue à l’utilisateur. Lors qu’on écrit un teste unitaire, on se concentre sur l’arbre d’éléments généré par UserProfile, sans tester l’implémentation du composant Badge.

L’exemple suivant montre comment vérifier dans un test si une instance du composant Badge avec les bons props est bien générée par le composant UserProfile.

import ShallowRenderer from 'react-test-renderer/shallow';

it('renders user badge', () => {
    const userProfile = ( 
        <UserProfile 
            userFirstName='Marcos' 
            userFamilyName='Almeida' 
            userLogin='almeida.marcos'
        />
    );

    const renderer = new ShallowRenderer();
    renderer.render(userProfile);

    const result = renderer.getRenderOutput();
    
    expect(result.props.children
        .some(child => child.type === Badge && child.props.name === 'Marcos'))
        .toBeTruthy();
});

A-t-il le bon comportement dans chaque étape de son cycle de vie?

Le ShallowRenderer est très bien pour tester le rendu d’un composant, malheureusement, il n’implémente pas tout cycle de vie React. Pour cela, nous allons utiliser le TestRenderer. Il génère un rendu complet du composant (y compris de ses sous composants) et fournit des méthodes telles que update() et unmount() qui peuvent être utilisées pour simuler les mises à jour et le démontage d’un composant.

Ici, on a un composant auquel on associe des fonctions Javascript à appeler pendant différentes étapes du cycle de vie :

export class EventFullComponent extends React.Component {
    componentWillMount() {
        this.props.doSomethingWhenMount();
    }

    componentWillUnmount() {
        this.props.doSomethingWhenUnmount();
    }

    render() {
        this.props.doSomethingWhenRender();
        return (
            <div>
                {this.props.saySomething}
            </div>
        );
    }
}

Pour le test réalisé ci-dessous, on crée un premier rendu (render) du composant (component). Après on le mets à jour, avec l’aide d’une autre instance (changedComponent) du composant avec la même key et enfin, on démonte le composant.

import * as TestRenderer from 'react-test-renderer';
it('reacts to lifecycle', () => {
    const component = ( 
          <EventFullComponent
              key='key'
              saySomething='something'
              doSomethingWhenMount={jest.fn()}
              doSomethingWhenUnmount={jest.fn()}
              doSomethingWhenRender={jest.fn()}
            />
    );

    const rendered = TestRenderer.create(component);

    expect(component.props.doSomethingWhenMount.mock.calls.length).toBe(1); // component mounted
    expect(component.props.doSomethingWhenRender.mock.calls.length).toBe(1); // component rendered
    expect(component.props.doSomethingWhenUnmount.mock.calls.length).toBe(0);

    const changedComponent = ( 
          <EventFullComponent
              key='key'
              saySomething='something'
              doSomethingWhenMount={component.props.doSomethingWhenMount}
              doSomethingWhenUnmount={component.props.doSomethingWhenUnmount}
              doSomethingWhenRender={component.props.doSomethingWhenRender}
            />
    );

    rendered.update(changedComponent);

    expect(component.props.doSomethingWhenMount.mock.calls.length).toBe(1); // component NOT re-mounted
    expect(component.props.doSomethingWhenRender.mock.calls.length).toBe(2); // component re-rendered 
    expect(component.props.doSomethingWhenUnmount.mock.calls.length).toBe(0);

    rendered.unmount();

    expect(component.props.doSomethingWhenMount.mock.calls.length).toBe(1);
    expect(component.props.doSomethingWhenRender.mock.calls.length).toBe(2);
    expect(component.props.doSomethingWhenUnmount.mock.calls.length).toBe(1); // component unmounted
});

Se comporte-t-il bien face à des événements UI?

Lorsque l’on veut écrire ce genre de tests, il faudra utiliser un type de renderer different de ceux qu’on a utilisé dans les sections précédentes. Il nous faut un renderer qui monte le composant testé dans un DOM (vrai ou simulé) et qui est donc capable de lui envoyer des événements. Dans cet article on utilisera le renderer renderIntoDocument() pour générer le rendu sur un DOM et l’utilitaire Simulate pour envoyer des événements à un composant React.

Considère le composant suivant, qui représente un compteur de clicks:

export class ClickCount extends React.Component {
    constructor() {
        super();
        this.state = {count: 0};
    }

    render() {
        return (
            <div>
                Count: {this.state.count}
                <input 
                    type='button' 
                    value='+' 
                    onClick={() => 
                        this.setState((prevState, props) => ({
                            count: prevState.count + 1
                        }))
                }/>
            </div>
        );
    }
}

Si on veut voir si le button ‘+’ fonctionne correctement, on simulera un événement click avec l’outil Simulate. On testera si le compteur de clicks était bien à 0 avant le click, et 1 après. Remarquez qu’on importe ReactDOM qui nous simulera un DOM sur lequel les composants seront crées.

import * as TestUtils from 'react-dom/test-utils';
import * as ReactDOM from 'react-dom';

it('reacts to events', () => {
    const component = (  ); 
    const renderedComponent = TestUtils.renderIntoDocument(component);
    const button = TestUtils.findRenderedDOMComponentWithTag(renderedComponent, 'input');

    expect(renderedComponent.state.count).toBe(0);

    TestUtils.Simulate.click(button);

    expect(renderedComponent.state.count).toBe(1);
});

Pour aller plus loin

Pour conclure, on a juste présenté une introduction rapide sur pourquoi et comment tester des composants React et quelques méthodes de mise du composant en situation d’être testé. Dans cet article on n’a parlé que des outils de l’API standard React. Pour aller plus loin, je vous propose quelques articles plus complets et quelques bibliothèques utiles:

Envie de poursuivre votre lecture ? Découvrez les autres articles de notre blog !