Des tests de composants avec Cypress

Merci beaucoup Anthony d'avoir pris le temps de coécrire cet article avec moi.

Introduction

Cypress est un framework JavaScript de “end to end testing”, il permet de tester le rendu et les actions utilisateurs de l'application à l'aide de scénarios. Ici, nous allons l’utiliser pour développer l’interface graphique de notre application à partir des tests, plus précisément des tests de composants. On va donc faire de l’UI en TDD !

Pour bien situer l’article, nous partons du principe que l’application testée est un Front-End autonome puisque nous n’allons pas aborder le fonctionnement des tests de composants côté Back-End.

Les tests de composants, c'est quoi ?

Leur place dans la pyramide des tests

Pyramide des tests

Cette version de la pyramide des tests montre les différents types de tests existants. Ceux qui se trouvent au plus bas de la pyramide ont une granularité faible, ce sont les moins coûteux, ils sont plus faciles à mettre en place, à maintenir et donnent un feedback rapide au développeur lorsqu’ils sont joués. Les tests situés dans le haut de la pyramide sont moins nombreux car leur granularité est plus importante, ils nécessitent le déploiement d’un environnement complet, ont un coût de maintenance plus élevé, s’exécutent plus lentement et sont plus fragiles. Cependant, leur pertinence est tout aussi importante.

Les tests de composants sont les derniers tests qui ne nécessitent pas forcément le déploiement de la stack complète. Dans notre cas, nous pouvons les utiliser pour guider notre implémentation, pour faire du TDD.

Différence entre tests de composants et tests “end to end”

Cypress se présente comme un framework de “end to end testing”. Les tests “end to end” permettent de tester toute la stack de notre application. C’est-à-dire les interactions entre le Front-End, Back-End et la persistance pour une application monolithique ou l’ensemble de nos services dans un contexte distribué.

Les tests de composants permettent de créer des scénarios isolés pour tester le comportement d’un service en simulant son utilisation par des utilisateurs. Contrairement aux tests “end to end”, dans le contexte d’une application Front-End, les tests de composants utilisent une stratégie de tests à base de stubs pour reproduire les interactions avec le Back End ou les autres services.

Les autres outils

Cypress n’est pas le seul outil permettant d’effectuer ce type de tests. Le tableau ci-dessous propose un comparatif non exhaustif avec d’autres solutions.

Cypress Nightwatch Protractor TestCafe
Rendu du navigateur Inclus Délégué via la norme Webdriver Délégué via WebdriverJS de Selenium Inclus
Support multi navigateur Non : Chrome Oui Oui Oui
Couplage à une solution Non Non Oui : Angular / Angular.js Non
Popularité (téléchargements + stars) Forte Moyenne Forte Faible

Premiers pas avec Cypress

Installation

npm install cypress --save-dev

Cette commande ajoute Cypress dans les node_modules de notre application et génère un dossier "cypress" contenant les éléments nécessaires pour l'utilisation de l'outil ainsi que des exemples.

Utilisation

Pour ouvrir Cypress, plusieurs possibilités :

  • Utiliser npx, un exécuteur de package inclus dans npm > v5.2 :
    > npx cypress open
    
  • Créer un script npm …
    {
      "scripts": {
        "test:component": "cypress open"
      }
    }
    

… pour pouvoir directement utiliser la commande :

> npm run test:component

On ouvre donc l’interface utilisateur suivante :

Interface utilisateur de Cypress

On retrouve l'ensemble des tests présents dans le dossier "integration". Pour jouer les tests, on clique sur le nom d'un test en particulier (qui correspond à un fichier) ou sur "Run all specs" pour tous les lancer.

Écrire son premier test de composants avec Cypress

À travers un exemple, nous allons montrer le fonctionnement de l’outil. La documentation de Cypress, plus exhaustive, permettra de compléter cet exemple.

Exemple

Voici donc le test d’une simple page composée du titre “Ma première utilisation de Cypress”.

describe("First simple page", () => {
  it("Should have a title", () => {
    cy.visit("/");
    cy.get("h1")
      .should("have.text", "Ma première utilisation de Cypress");
  });
});

Si vous êtes familier avec les tests Front-End avec des frameworks comme Jest ou Mocha, vous retrouvez les expressions describe() et it(). Cypress se base sur la syntaxe de Mocha. On retrouve également context(), before(), beforeEach(), afterEach(), after(), .only() et .skip().

Comment ça fonctionne ?

Avec cy.visit("/");, on spécifie l’URL à visiter. La base URL (http://localhost:3000 par exemple) peut être configurée pour ne pas avoir à la spécifier à chaque fois. Cette configuration se fait dans le fichier cypress.json en spécifiant la baseUrl :

{
  "baseUrl": "http://localhost:8080"
}

La commande cy.get("h1") cherche un élément du DOM. Ici nous cherchons directement l’élément titre h1 mais il est conseillé d’utiliser un sélecteur personnalisé. Il est également possible de chercher la classe ou l’id.

Finalement, notre assertion .should("have.text", "Ma première utilisation de Cypress") vérifie que l’élément trouvé contient bien le texte souhaité.

Stubber les routes

Comme nous l’avons vu, notre application peut interagir avec un serveur via une ou plusieurs API. Nous devons donc définir ce que retournent ces API externes via le système de Stubbing.

Cypress permet à l’aide de son cy.server() de stubber un serveur HTTP.

Avec cy.server({ enable: false }), vous pouvez arrêter votre serveur de stubs. C’est utile si vous avez envie de définir un stub à un moment et reprendre le fonctionnement normal de votre application par la suite.

Une fois le serveur lancé, il nous reste à stubber nos routes avec cy.route().

Pour récupérer le contenu du JSON retourné, il est possible d’utiliser les fixtures avec cy.fixture().

Les routes sont très faciles à stubber lorsque vous avez une ressource par URI (pour du REST par exemple), en revanche, Cypress ne propose rien pour gérer les stubs GraphQL. Une solution alternative est discutée dans l’issue GitHub suivante : https://github.com/cypress-io/cypress-documentation/issues/122.

Exemple classique :

cy.server();
cy.route({
  method: 'GET',        /* Route all GET requests                  */
  url: '/user/*',       /* that have a URL that matches '/users/*' */
  response: {           /* and force the response to be: {         */
    firstname: 'John',  /*   "firstname": "John",                  */
    lastname: 'Doe',    /*   "lastname": "Doe"                     */
  },                    /* }                                       */
}).as('user');

Exemple avec fixture :

cy.server();

// Set the response to be the user.json fixture
cy.route('GET', 'user/*', 'fixture:user.json').as('user');

L’appel à as permet de nommer notre route pour l’identifier et cy.wait() utilise cet identifiant pour attendre le résultat de la requête appelée par notre composant :

cy.server();

// Add route for the user John Doe
cy.route('GET', 'user/*', 'fixture:user.json').as('user');

// Profile page call current user
cy.visit('/profile');

// Wait the user result
cy.wait('@user');

// Check h1 is the combination of firstname and lastname
cy.get('h1').should('have.text', 'John Doe');

Conclusion

Bien que Cypress se définisse comme un framework de test “end to end”, cet outil nous permet d’écrire des tests de composants. Nous recommandons son utilisation pour toutes vos applications Front-End : il se marie bien avec Vue.js, Angular, React ...

Le fait de ne pas déployer notre stack nous permet de faire du TDD (et même de l’outside-in TDD), nous reviendrons donc là-dessus dans notre prochain article.