3 juin 2019
Tester son app React avec Cypress
7 minutes de lecture
Combien de fois avez-vous testé manuellement un workflow dans votre application pour vous assurer que tout fonctionne ? 5, 10 fois, plus ? Pourquoi ne pas prendre un peu de temps pour automatiser ces tests et vous éviter cette tâche fastidieuse et ainsi :
- 💪 Gagner en confiance ;
- ☔️ Prévenir les bugs ;
- ⏳ Gagner du temps ;
- 💎 Augmenter la qualité de vos applications.
Les tests end to end (E2E) permettent de tester votre application comme un utilisateur le ferait. Ces tests sont plus faciles à mettre en place car ils s'émancipent des détails d'implémentation. Encore faut-il avoir le bon outil 🙃. Cypress est une suite de test dédiée dans les tests E2E.
Voyons comment la mettre en place au travers de quatres parties :
- Installer Cypress dans votre projet React ;
- Écrire vos tests ;
- Mocker un appel HTTP ;
- Lancer vos tests sur votre CI.
Installer Cypress
Nous allons commencer par ajouter Cypress à nos dépendances de développement avec un traditionnel :
yarn add --dev cypress
Puis ajouter dans la partie scripts de notre package.json
la commande suivante :
{
"scripts": {
"start": "react-scripts start",
"cypress:open": "cypress open"
}
}
Lorsque vous exécutez un binaire depuis un script, node va automatiquement chercher dans le
répertoire ./node_modules/.bin/
Cette commande va lancer le test runner en mode interactif. Lors de la première exécution, Cypress créé un dossier cypress
à la racine du projet avec notamment des exemples de tests :
Il est maintenant temps de tester notre application ! L'application en question permet de rechercher des films via l'API de IMDB et retourner le premier film de la liste.
Vous pouvez retrouver cette application sur CodeSandbox.
Écrire les tests
Boostrap les tests
Tout d'abord il nous faut un plan de test. Avec des tests E2E, il est assez naturel de le trouver : il suffit de se mettre dans la place de l'utilisateur et tester les différents scénarios possibles.
Voici un des scénarios (le happy path) :
- ✅ Je remplis mon champ de recherche avec un film existant ;
- ✅ Je clique sur le bouton ;
- ✅ Je vérifie que la fiche du film se soit bien rendue.
Les tests e2e nécessitent que l'application à tester soit disponible, il nous faut donc la servir avant de lancer les tests. Commençons donc par indiquer l'adresse de notre serveur sur lequel nos tests seront effectués. Pour cela, nous allons passer par le fichier de configuration cypress.json
à la racine de votre projets :
{
"baseUrl": "http://localhost:3042"
}
De cette manière nous n'aurons pas à indiquer l'url pour chaque test. Créons ensuite notre fichier cypress/integration/search-movie.spec.js
. Nous indiquons l'url relative à tester dans la méthode beforeEach
. Comme nous n'avons pas de routing, il s'agit ici de l'index. Enfin, nous déclarons une méthode it
correspondante à notre test :
context('Movie search', () => {
beforeEach(() => {
cy.visit('/')
})
it('Search movie success', () => {
// test
})
})
Les sélecteurs
Écrivons maintenant notre test. La première chose à faire est de sélectionner notre champ de recherche. Pour cela, il nous faut un moyen de l'identifier dans le DOM : nous pouvons ajouter un attribut name
ou id
mais une bonne pratique consiste à passer par des attributs data-testid
dédiés aux tests. Ainsi ces derniers restent découplés de la logique applicative.
Nous ajoutons donc à notre champ l'attribut data-testid
:
<InputBase
data-testid="input-search"
onChange={handleOnChange}
className={classes.input}
placeholder="Search Movies 🍿"
/>
Dans notre test, nous pouvons le récupérer avec le sélecteur suivant :
cy.get('[data-testid=input-search]')
Cette syntaxe est un peu verbeuse, c'est pourquoi je conseille d'utiliser les commandes cypress-testing-library permettant de récupérer vos éléments plus simplement. Pour cela, ajoutez la librairie :
yarn add --dev @testing-library/cypress
Puis importez les commandes dans le fichier cypress/support/commands.js
import '@testing-library/cypress/add-commands'
Vous pouvez dès lors utiliser la méthode getByTestId
:
cy.getByTestId('input-search')
Nous pouvons maintenant écrire notre test et ajouter des attributs data-testid
sur les élements du DOM qui nous intéressent :
context('Movie search', () => {
beforeEach(() => {
cy.visit('/')
})
it('Search movie form', () => {
cy.getByTestId('input-search')
.get('input')
.type('the shining')
cy.getByTestId('button-search').click()
cy.getByTestId('movie-title').contains('The Shining')
cy.getByTestId('movie-year').contains('1980')
})
})
Ici nous remplissons le champ avec la valeur the shining
, cliquons sur le bouton et vérifions que les bonnes informations ont été rendues. Le test runner devrait automatiquement lancer les tests à chaque modification :
Les tests sont verts 🌱. La commande cypress:open
est pratique en cours de développement : elle donne un retour visuel et instantané sur vos tests. Mais vous pouvez également les lancer directement dans la console grâce la commande cypress run
. Cette dernière, va jouer vos tests dans un navigateur headless (Electron) sans ouvrir de fenêtre.
En mode headless, Cypress génère automatiquement des vidéos de vos tests afin que vous puissiez avoir un retour visuel en cas d'erreur. Vous pouvez désactiver ces vidéos en modifiant votre fichier de configuration cypress.json
:
{
"baseUrl": "http://localhost:3000"
"video":false
}
Mocker l'appel à l'API
En l'état lorsque notre test est exécuté, un appel HTTP est fait à l'API d'IMDB. Il serait intéressant de créer un mock de cette API, c'est à dire simuler une réponse afin que nos tests ne dépendent pas d'une API externe. Pour cela nous allons utiliser les méthodes server()
et route()
:
context('Movie search', () => {
beforeEach(() => {
cy.visit('/')
})
it('Search movie form', () => {
cy.server()
.route(
'GET',
'https://www.omdbapi.com/?apikey=4d7c0bd5&s=the shining',
'fixture:omdbapi-response.json'
)
.as('fetch-movie')
cy.getByTestId('input-search > input').type('the shining')
cy.getByTestId('button-search')
.click()
.wait('@fetch-movie')
cy.getByTestId('movie-title').contains('The Shining')
cy.getByTestId('movie-year').contains('1980')
})
})
🚨 Attention à ce que l'url de la méthode route soit exactement la même que celle appelée. J'ai copié/collé l'url depuis Firefox qui contenait %20
au lieu de l'espace, empêchant Cypress d'intercepter la requête :
-https://www.omdbapi.com/?apikey=4d7c0bd5&s=the%20shining
+https://www.omdbapi.com/?apikey=4d7c0bd5&s=the shining
La méthode as('fetch-movie')
permet de nommer la requête et ainsi indiquer que nous voulons mocker celle-ci grâce à la méthode wait('@fetch-movie')
juste après le clic.
Enfin nous renseignons par l'intermédiaire de fixture:omdbapi-response.json
la réponse de la requête. Le fichier omdbapi-response.json
est à placer dans le dossier cypress/fixtures
:
{
"Search": [
{
"Title": "The Shining",
"Year": "1980",
"imdbID": "tt0081505",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BZWFlYmY2MGEtZjVkYS00YzU4LTg0YjQtYzY1ZGE3NTA5NGQxXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg"
}
],
"totalResults": "43",
"Response": "True"
}
Le cas de fetch
Dans notre application, nous utilisons la méthode fetch, malheureusement Cypress ne supporte que l'interception de requêtes en mode XHR. Il faut donc forcer Cypress à utiliser XHR plutôt que fetch. Si vous utilisez axios comme client HTTP, vous n'aurez pas de soucis car la librairie utilise XHR en coulisse.
Nous allons donc installer le polyfill whatwg-fetch (qui est de toute façon une bonne pratique) et l'inclure dans le fichier root de votre application :
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import 'whatwg-fetch'
ReactDOM.render(<App />, document.getElementById('root'))
Nous devons alors désactiver fetch dans le cadre de nos tests pour que Cypress utilise le polyfill (et donc XHR) :
context('Movie search', () => {
beforeEach(() => {
cy.visit('/', {
onBeforeLoad: win => {
win.fetch = null
},
})
})
it('Search movie form', () => {
cy.server().route(
'GET',
'https://www.omdbapi.com/?apikey=4d7c0bd5&s=the shining',
'fixture:omdbapi-response.json'
)
cy.as('fetch-movie')
cy.getByTestId('input-search > input').type('the shining')
cy.getByTestId('button-search')
.click()
.wait('@fetch-movie')
cy.getByTestId('movie-title').contains('The Shining')
cy.getByTestId('movie-year').contains('1980')
})
})
La requête devrait être interceptée et vos tests verts ✅
Lancez vos tests sur votre CI
Écrire des test c'est bien, mais les exécuter sur un service d'intégration continue c'est mieux ! Ils permettent d'avoir un retour automatique sur chaque PR, branche et forcer l'équipe à écrire et corriger les tests.
En local, vous exécutez vos tests sur votre serveur de développement mais dans le cadre d'un CI, il vous faudra lancer votre application. Pour cela on peut utiliser le package concurrently permettant de lancer des commandes de manière concurrente.
yarn add --dev concurrently
Puis ajoutez dans vos scripts une commande combinant yarn start
et yarn test:e2e
:
{
"scripts": {
"start": "PORT=3042 react-scripts start",
"test:e2e": "cypress run",
"test:e2e:ci": "concurrently --kill-others --success=first 'yarn start --progress=false --no-info' 'yarn test:e2e'",
}
}
Il manque plus qu'à configurer notre CI (ici notre fichier .travis.yml
pour Travis) :
language: node_js
node_js:
- 'stable'
addons:
apt:
packages:
# For cypress
- libgconf-2-4
cache:
yarn: true
directories:
- ~/.cache
script:
- yarn test:e2e:ci
Le package libgconf-2-4
est nécessaire à Cypress et la configuration du cache permet de cacher le binaire de Cypress. Vous pouvez trouver plus d'informations sur la doc de Cypress.
Et ensuite ?
Nous avons abordé une petite partie de la suite de tests Cypress. Comme d'habitude, je vous invite à lire la documentation de l'outil, fourmillante d'informations utiles.
Dans la continuité des tests E2E, vous pouvez mettre en place des tests fonctionnels grâce à la librairie react-testing-library qui est d'ailleurs recommandée dans la documentation de create-react-app.
Le code de cet article est disponible sur CodeSandbox. À vos tests ! ✅