Tests end-to-end avec Navalia & Jest
Il est souvent long et fastidieux de réaliser des tests “end2end”. De nombreuses solutions comme Sélénium sont disponibles et lèvent souvent plusieurs difficultés sur leur mise en oeuvre :
- Importante courbe d’apprentissage (langage dédié) ;
- Configuration difficile ;
- Tests difficilement scalables ;
- Très long en temps d’exécution ;
- La réutilisabilité des tests difficile voire impossible.
Parti de ces constats, Joel Griffith décide de développer Navalia, un framework open-source permettant d’automatiser facilement des actions utilisateur sur des navigateurs “headless”(*) et facilement scalable. La tagline du projet est :
Simplicity and performance. Browser automation should be an enjoyable experience and not a painful one.
Navalia
Navalia est une surcouche d’exécution à chrome headless proposant une API Javascript, très simple à prendre en main, pour réaliser vos actions sur le navigateur. Et si vous ne faites pas de Javascript, Navalia met à disposition un serveur GraphQL, afin d’exécuter vos tests sur Chrome via HTTP.
L’API proposée par Navalia, permet de réaliser :
- différentes actions utilisateur : naviguer, cliquer, remplir des formulaires, scroller…
- des tests de validité : valider l’existence de libellé, récupérer des éléments du DOM…
- des exports du rendu : par des captures d’écrans, exports PDF, sauvegardes de la page HTML courante…
- des actions navigateur : suppression du cache et des cookies, gestion de l’historique navigateur, rafraîchissement des pages…
De plus, la grande force de Navalia est sa capacité à pouvoir réaliser vos tests en parallèle sur plusieurs instances, dans plusieurs onglets du navigateur et ainsi de réduire le temps d’exécution de vos tests.
Le pré-requis pour utiliser Navalia est d’avoir au minimum un Chrome ou Chromium supportant le headless (à partir de la version 59). Ensuite il suffit de l’installer en dépendance de développement de votre projet via yarn ou npm :
$ yarn add navalia -D
A partir de là vous pouvez coder vos actions utilisateur en javascript :
const { Chrome } = require('navalia'); const chrome = new Chrome(); chrome .goto('https://amazon.com') .type('input', 'Kindle') .click('.buy-now') .end() .then((responses) => { console.log(responses); // ['https://www.amazon.com/', true, true, true] });
Comme vous le voyez sur cet exemple de code, Navalia propose une API simple et chainable. Et elle est encore plus sympa à utiliser avec async / await :
const { Chrome } = require('navalia'); const chrome = new Chrome(); async function buyItOnAmazon() { const url = await chrome.goto('https://amazon.com'); const typed = await chrome.type('input', 'Kindle'); const clicked = await chrome.click('.buy-now'); chrome.done(); console.log(url, typed, clicked); // 'https://www.amazon.com/', true, true } buyItOnAmazon();
Intégration de Navalia avec Jest
Pour orchestrer et instrumenter l’exécution de nos tests, un exécuteur de test est alors nécessaire. Étant un développeur React, j’utilise Jest pour réaliser les tests unitaires de mes composants, il devient alors le candidat idéal pour exécuter mes tests end2end.
Prenons pour exemple une simple application de TODO liste réalisée en React :
import React, { Component } from "react"; import "./app.css"; class App extends Component { state = { todos: [], current: undefined }; updateCurrent = e => this.setState({ current: e.target.value }); addTodo = e => { this.setState(state => ({ todos: [...state.todos, state.current] })); }; deleteTodo = index => () => { const toDelete = [...this.state.todos]; toDelete.splice(index, 1); this.setState({ todos: toDelete }); }; render() { return ( <div className="app"> <h1>TODOs</h1> <div className="addTodo"> <input id="newTodoInput" type="text" placeholder="add todo" onChange={this.updateCurrent} /> <button id="addBtn" onClick={this.addTodo}> <i className="fa fa-plus" /> </button> </div> <div className="todolist"> {this.state.todos.length === 0 && <div id="nothing" className="todo"> you have nothing to do </div>} {this.state.todos.map((todo, i) => <div key={i} className="todo"> <span id={`todo-${i}`}> {todo} </span> <a id={`deleteBtn-${i}`} onClick={this.deleteTodo(i)}> <i className="fa fa-trash" /> </a> </div> )} </div> </div> ); } } export default App;
Avec Jest nous pouvons alors commencer à créer et organiser nos tests. Comme vous pouvez le voir, il est très simple de factoriser les actions utilisateurs (dans ce cas la création de X todos avant chaque test) :
const { Chrome } = require('navalia') describe('todolist', () => { let chrome beforeEach(() => { chrome = new Chrome() }) afterEach(() => { chrome.done() }) const addTodo = async (chrome, nbTodos) => { for (let i = 0; i < nbTodos; i++) { await chrome.type('#newTodoInput', `my todo ${i}`) await chrome.click('#addBtn') expect(await chrome.exists(`#todo-${i}`)).toBe(true) } } it('should have nothing by default', async () => { await chrome.goto('http://localhost:8000') expect(await chrome.exists('#nothing')).toBe(true) expect(await chrome.text('#nothing')).toBe('you have nothing to do') }) it('should add todos', async () => { await chrome.goto('http://localhost:8000') await addTodo(chrome, 4) }) it('should add and remove todos', async () => { await chrome.goto('http://localhost:8000') await addTodo(chrome, 1) await chrome.click('#deleteBtn-0') expect(await chrome.exists('#todo-0')).toBe(false) expect(await chrome.exists('#nothing')).toBe(true) }) })
Et voilà le résultat :
Nous avons pu réaliser des tests sur le comportement de notre application dans un navigateur Chrome en fonction de scénarios utilisateurs tout en factorisant certaines actions.
Utilisation de jest-image-snapshot
Maintenant, comment faire si je souhaite vérifier le rendu de mon application face aux interactions de l’utilisateur ?
Pour cela, Navalia donne la possibilité de réaliser des exports du rendu des pages web en png ou pdf. Combiné à jest-image-snapshot (plugin Jest d’American Express permettant de réaliser des comparaisons visuelles d’image à la façon des snapshots de Jest), nous avons la possibilité d’automatiser les tests de rendu visuel de notre application et nos composants.
Pour cela, il suffit d’installer le plugin jest via :
$ yarn add jest-image-snapshot -D
Déclarer le plugin auprès de Jest et développer des tests sur le rendu de nos composants :
const { Chrome } = require('navalia') const { toMatchImageSnapshot } = require('jest-image-snapshot') expect.extend({ toMatchImageSnapshot }) describe('todolist', () => { let chrome beforeEach(() => { chrome = new Chrome() }) afterEach(() => { chrome.done() }) const addTodo = async (chrome, nbTodos) => { for (let i = 0; i < nbTodos; i++) { await chrome.type('#newTodoInput', `my todo ${i}`) await chrome.click('#addBtn') expect(await chrome.exists(`#todo-${i}`)).toBe(true) } } it('test todolist page default rendering', async () => { await chrome.goto('http://localhost:8000') const image = await chrome.screenshot() expect(image).toMatchImageSnapshot() }) it('test todolist nothing panel default rendering', async () => { await chrome.goto('http://localhost:8000') const image = await chrome.screenshot() expect(image).toMatchImageSnapshot() }) it('test todolist rendering with todos', async () => { await chrome.goto('http://localhost:8000') await addTodo(chrome, 4) const image = await chrome.screenshot() expect(image).toMatchImageSnapshot() }) })
Si un rendu inattendu (par exemple un fichier CSS est modifié par erreur) est identifié, votre test sera en erreur et vous aurez accès au diff entre le rendu attendu et le rendu actuel :
L’image du haut est le rendu actuel, au milieu de diff et en bas le résultat attendu.
Navalia donne la possibilité d’exécuter Chrome en utilisant différentes résolutions via chrome.size() , il est donc possible d’écrire un test de rendu de nos composants pour ces résolutions et de s’assurer de la non régression visuelle de notre application.
Optimisation des tests
Navalia et Jest proposent des mécanismes afin d’optimiser le temps d’exécution de nos tests.
Navalia donne la possibilité d’exécuter les tests sur plusieurs instances de chrome en parallèles et sur plusieurs onglets par instances. Combiné à l’exécution concurrentielle des tests de Jest (it.concurrent ), le temps d’exécution est grandement amélioré.
const { Navalia } = require('navalia'); const navalia = new Navalia({ numInstances: 3 }); describe('todolist', () => { let chrome afterAll(() => { return navalia.kill(); }); const addTodo = async (chrome, nbTodos) => { for (let i = 0; i < nbTodos; i++) { await chrome.type('#newTodoInput', `my todo ${i}`) await chrome.click('#addBtn') expect(await chrome.exists(`#todo-${i}`)).toBe(true) } } it.concurrent('should have nothing by default', async () => { await navalia.run(async (chrome) => { await chrome.goto('http://localhost:8000') expect(await chrome.exists('#nothing')).toBe(true) expect(await chrome.text('#nothing')).toBe('you have nothing to do') }); }) it.concurrent('should add todos', async () => { await navalia.run(async (chrome) => { await chrome.goto('http://localhost:8000') await addTodo(chrome, 4) }); }) it.concurrent('should add and remove todos', async () => { await navalia.run(async (chrome) => { await chrome.goto('http://localhost:8000') await addTodo(chrome, 1) await chrome.click('#deleteBtn-0') expect(await chrome.exists('#todo-0')).toBe(false) expect(await chrome.exists('#nothing')).toBe(true) }); }) })
Et voilà, quelques précieuses secondes de gagnées sur l’exécution de nos tests 🙂
(*) pour le moment, seul Chrome est supporté, mais d’autres navigateurs sont prévus.
sources:
Intéressant cet outil, je ne connaissais pas !
J’aime beaucoup l’outil de comparaison du visuel!
Je partage les difficultés suivantes sur Selenium:
– Tests difficilement scalables ;
– Très long en temps d’exécution ;
Pour les autres points, avec le pattern page object, il est possible de bien s’en sortir.
Je rajouterais que Selenium ne couvre pas ou mal les applications Single Page Application.
À ce sujet est ce que Navalia va détrôner Protractor grâce à ses nouvelles fonctionnalités?