Table Des Matières
Tester Les Composants React Avec Jest & Enzyme
Dans les tous derneirs articles concernant React, je vous ai montrer dans un premier article comment combiner React avec Socket.io pour avoir des applications en temps réel, dans second article j’ai traité le sujet relatif aux flux de données dans React-Redux, dans un troisième article, j’ai parlé plus en profondeur sur à quoi ce que chacune des fonctions de React-Redux ressemble et comment le tout fonctionne.
Certaines personnes disent que tester les composants React est un fait inutile et dans de nombreux cas, ils ont bien raison, mais il y a des cas où je pense que c’est vraiment utile, comme le cas des bibliothèques de composants, les projets source libre (open source), aussi le cas d’une intégration avec des composants tiers et enfin le cas des bogues (bugs), pour éviter les régressions.
J’ai essayé de nombreux outils et finalement j’ai pu trouver une combinaison fiable dans le but de vous epargner les taches lourdes de recherches et de tests, oui c’est une combinaison d’outils que j’aime assez pour la suggérer tranquillement et sans souci à d’autres développeurs:
- Jest, une solution complète pour les test JavaScript, facile à installer et configurer. Elle fonctionne inhabituellement pour tout projet React.
- Enzyme est un utilitaire de test JavaScript pour React qui facilite l’affirmation, la manipulation et la traversée des sorties de vos composants React.
- Enzyme-to-Json pour convertir les enveloppes d’enzymes pour ce qu’on appelle Jest snapshot matcher.
Pour la plupart de mes tests, j’utilise un rendu superficiel avec des Jest snapshots.
Rendu superficiel
Le rendu peu profond ou superficiel (Shallow rendering) rend le composant (lui-même seule) sans ses enfants. Donc, si vous changez quelque chose dans un composant enfant, cela ne changera pas la sortie superficielle de votre composant. Ou un bogue, introduit dans un composant enfant, ne va pas casser le test de votre composant. Il ne nécessite pas non plus DOM.
On a cet exemple de composants
const ButtonWithIcon = ({icon, children}) => ( <button><Icon icon={icon} />{children}</button> );
Sera rendu par React comme ceci:
<button> <i class="icon icon_coffee"></i> Hello Jest! </button>
Regardez maintenant ce que ça va donner avec un rendu superficiel
<button> <Icon icon="coffee" /> Hello Jest! </button>
Notez que le composant Icon n’a pas été rendu.
Test Instantané (Snapshot)
Les instantanés (Snapshot) de type Jest ressemblent aux anciennes UI de texte avec des fenêtres et des boutons constitués de caractères de texte: il s’agit d’une sortie rendue de votre composant stockée dans un fichier texte.
Vous dites à Jest que vous voulez être sûr que la sortie de ce composant ne devrait jamais changer accidentellement et que Jest l’enregistre dans un fichier qui ressemble à ceci:
exports[`test should render a label 1`] = ` <label> Hello Jest! </label> `; exports[`test should render a small label 1`] = ` <label> Hello Jest! </label> `;
Chaque fois que vous modifiez votre balisage, Jest vous montrera un diff et vous demandera de mettre à jour lz snapshot (l’instantané) si le changement était prévu.
Jest stocke les instantanés à côté de vos tests dans des fichiers comme: __snapshots__/Label.spec.js.snap. Et vous devez les valider avec votre code.
Pourquoi Jest
Très rapide, les tests y sont instantanés (snapshat), le mode de veille interactif est impressionnant qui ne réexécute que les tests pertinents pour vos modifications, les messages d’échec sont utiles, sa configuration est très simple, mocks et espions, rapport de couverture avec un seul commutateur de ligne de commande, développement actif, Enfin, impossible d’y écrire silencieusement de mauvaise affirmations comme: expect(foo).to.be.a.function au lieu de: expect(foo).to.be.a(‘function’) dans Chai, parce que c’est la seule chose naturelle à écrire après: expect(foo).to.be.true. (correcte).
Pourquoi Enzyme
Il y a des utilitaires pratiques pour travailler avec un rendu superficiel, un balisage rendu statique ou un rendu DOM, et des API ressemblant à jQuery pour trouver des éléments, lire des props (accessoires), etc.
Configuration
Commencez par installer toutes les dépendances, y compris les dépendances entre homologues (peer dependencies):
npm install --save-dev jest react-test-renderer enzyme enzyme-adapter-react-16 enzyme-to-json
Vous aurez également besoin de babel-jest pour Babel et de ts-jest pour TypeScript.
Mettez à jour votre package.json:
"scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }, "jest": { "setupFiles": ["./test/jestsetup.js"], "snapshotSerializers": ["enzyme-to-json/serializer"] }
snapshotSerializers vous permet de passer des enveloppes d’enzymes (Enzyme wrappers) directement au programme d’instantané de Jest (Jest’s snapshot matcher), sans les convertir manuellement en appelant la fonction toJson d’enzyme-to-json.
Créez un fichier test / jestsetup.js pour personnaliser l’environnement Jest (voir les fichiers d’installation ci-dessus):
import Enzyme, { shallow, render, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; // React 16 Enzyme adapter Enzyme.configure({ adapter: new Adapter() }); // Make Enzyme functions available in all test files without importing global.shallow = shallow; global.render = render; global.mount = mount;
Pour les modules CSS ajoutez également à la section jest dans votre package.json:
"jest": { "moduleNameMapper": { "^.+\\.(css|scss)$": "identity-obj-proxy" } }
Et ensuite exécutez:
npm install --save-dev identity-obj-proxy
Notez que identity-obj-proxy nécessite un node — harmony-proxies flag pour les nœuds (node ) 4 et 5.
Les Tests d’écriture
Test du rendu de composant de base
C’est suffisant pour la plupart des composants non interactifs:
test('render a label', () => { const wrapper = shallow( <Label>Hello Jest!</Label> ); expect(wrapper).toMatchSnapshot(); }); test('render a small label', () => { const wrapper = shallow( <Label small>Hello Jest!</Label> ); expect(wrapper).toMatchSnapshot(); }); test('render a grayish label', () => { const wrapper = shallow( <Label light>Hello Jest!</Label> ); expect(wrapper).toMatchSnapshot(); });
Test des props (accessoires)
Parfois, vous voulez être plus explicite et voir des valeurs réelles dans les tests. Dans ce cas, utilisez l’API Enzyme avec des assertions (affirmations) Jest régulières:
test('render a document title', () => { const wrapper = shallow( <DocumentTitle title="Events" /> ); expect(wrapper.prop('title')).toEqual('Events'); }); test('render a document title and a parent title', () => { const wrapper = shallow( <DocumentTitle title="Events" parent="Event Radar" /> ); expect(wrapper.prop('title')).toEqual('Events — Event Radar'); });
Dans certains cas, vous ne pouvez pas utiliser les snapshots (instantanés). Par exemple si vous avez des ID aléatoires ou quelque chose comme ça:
test('render a popover with a random ID', () => { const wrapper = shallow( <Popover>Hello Jest!</Popover> ); expect(wrapper.prop('id')).toMatch(/Popover\d+/); });
Tests d’événements
Vous pouvez simuler un événement tel que cliquer ou modifier, puis comparer un composant à un snapshot (instantané):
test('render Markdown in preview mode', () => { const wrapper = shallow( <MarkdownEditor value="*Hello* Jest!" /> ); expect(wrapper).toMatchSnapshot(); wrapper.find('[name="toggle-preview"]').simulate('click'); expect(wrapper).toMatchSnapshot(); });
Parfois, vous voulez interagir avec un élément d’un composant enfant pour tester l’effet dans votre composant. Pour cela, vous avez besoin d’un rendu DOM correct avec la méthode de montage d’Enzyme (Enzyme’s mount method):
test('open a code editor', () => { const wrapper = mount( <Playground code={code} /> ); expect(wrapper.find('.ReactCodeMirror')).toHaveLength(0); wrapper.find('button').simulate('click'); expect(wrapper.find('.ReactCodeMirror')).toHaveLength(1); });
Test des gestionnaires d’événements
Similaire au test des événements, mais au lieu de tester la sortie rendue par le composant avec un snapshot, utilisez la fonction mock de Jest pour tester un gestionnaire d’événements lui-même:
test('pass a selected value to the onChange handler', () => { const value = '2'; const onChange = jest.fn(); const wrapper = shallow( <Select items={ITEMS} onChange={onChange} /> ); expect(wrapper).toMatchSnapshot(); wrapper.find('select').simulate('change', { target: { value }, }); expect(onChange).toBeCalledWith(value); });
Pas seulement JSX
Les snapshots Jest fonctionnent avec JSON afin que vous puissiez tester toute fonction qui renvoie JSON de la même manière que vous testez vos composants:
test('accept custom properties', () => { const wrapper = shallow( <Layout flexBasis={0} flexGrow={1} flexShrink={1} flexWrap="wrap" justifyContent="flex-end" alignContent="center" alignItems="center" /> ); expect(wrapper.prop('style')).toMatchSnapshot(); });
Débogage et dépannage
Débogage de la sortie de rendu superficiel
Utilisez la méthode de débogage Enzyme pour imprimer une sortie de rendu superficielle:
const wrapper = shallow(/*~*/); console.log(wrapper.debug());
Tests échoués avec couverture autorisée
Lorsque vos tests échouent avec – l’indicateur de couverture avec diff comme ceci:
-<Button
+<Component
Essayez de remplacer le composant de fonction de flèche avec la fonction régulière:
- export default const Button = ({ children }) => { + export default function Button({ children }) {
Erreur requestAnimationFrame
Vous pouvez voir une erreur comme celle-ci lorsque vous exécutez vos tests:
console.error node_modules/fbjs/lib/warning.js:42 Warning: React depends on requestAnimationFrame. Make sure that you load a polyfill in older browsers. http://fb.me/react-polyfills
React 16 dépend de requestAnimationFrame, vous devez donc ajouter un polyfill à vos tests:
// test/jestsetup.js import 'raf/polyfill';
Ressources
- Jest cheat sheet (Anglais)
- Test des applications React par Max Stoiber (Anglais)
- Migration vers Jest par Kent C. Dodds (Anglais)
- Migration d’Ava à Jest par Jason Brown (Anglais)
Source: Article traduit depuis sa version originale en anglais de son auteur Artem Sapegin, développeur frontend, photographe passionné et créateur de React Styleguidist.