Testez vos SPA avec Cypress

J’ai découvert Cypress pendant que je faisais mes recherches pour l’article d’introduction aux tests de bout en bout. Disons que ça a été une révélation pour moi. Je pense que Cypress est le genre d’outil qu’on prend du plaisir à utiliser, et à explorer les possibilités.

J’ai donc décidé de consacrer ce premier cas pratique à Cypress.

Cypress? C’est quoi ça ?

Comme les auteurs le définissent, Cypress est un framework JavaScript pour les tests de bout en bout. Moi, je dirai un peu plus ; Cypress, c’est un agréable framework pour tester les applications JavaScript.

La philosophie

La philosophie derrière Cypress, c’est d’améliorer l’expérience des tests (automatisés) pour les développeurs. Et cette philosophie se traduit très bien dans l’outil, avec la mise à disposition de fonctionnalités qui vont dans ce sens.

Page de fonctionnalités Cypress
Page de fonctionnalités Cypress

Depuis l’écriture des tests à leur exécution en passant par le débogage, tout est optimisé pour rendre l’expérience la plus agréable possible.

Un exemple rapide, c’est l’interface d’exécution des tests de Cypress. C’est à ce jour l’interface la plus élaborée, mais aussi la plus accessible que j’ai utilisée. On y retrouve pleins de petites fonctionnalités intéressantes, qui bien combinées ou utilisées vont permettre de rendre l’exécution et le débogage des tests beaucoup plus aisés.

Interface d'exécution de tests de Cypress
Interface d’exécution de tests de Cypress

Nous verrons cela en détail plus loin.

Comment ça marche ?

Cypress, comment ça marche ?
Cypress, comment ça marche

Cette image résume assez bien comment marche Cypress. L’idée, c’est qu’il intègre à la fois :

  • Un exécutant de test (test runner)
  • Un outil de contrôle du navigateur
  • Des librairies d’assertions

Plus concrètement, Cypress utilise Mocha comme framework de BDD. Cela lui permet d’utiliser les syntaxes comme describe(), it(), before(), skip() pour décrire les tests et leurs comportements. Ensuite, la puissante librairie Chai est utilisée pour effectuer des assertions sur les tests.

💡

Lisez ici pour en savoir plus sur les autres librairies que Cypress utilise nativement.

En termes de fonctionnement, Cypress s’exécute ensemble avec l’application qui est testée. Celui lui permet entre autres d’être très rapide, mais aussi d’avoir un accès direct à l’application, et donc de se passer de certains niveaux d’abstraction en écrivant des tests.

Autre chose intéressante, Cypress est capable d’intercepter, de modifier, de rediriger tout le trafic qui entre et sort de l’application. C’est cette capacité notamment qui permet au framework de par exemple de se connecter programmatiquement à une application ; et donc d’éliminer ce temps de connexion dans tous les tests exécutés.

Les auteurs de Cypress ont mis un point d’honneur à avoir une documentation très fournie. Je vous invite donc à lire celle-ci sur le fonctionnement de Cypress.

Les cas d’usage et les limitations

Comme tout outil ou technologie, il y a des contextes dans lesquels Cypress va très bien performer ; et dans d’autres, ce ne sera pas du tout l’outil adapté.

Cypress est très adapté pour créer et automatiser les tests de bout en bout pour les applications JavaScript. C’est un peu son corps de métier.

Par contre, ce ne sera pas le meilleur outil pour faire du scrapping sur un site par exemple, ou encore pour des tests de performance.

En plus de cela, il y a des limitations qui sont inhérentes à l’outil, et dont il faut prendre compte avant de l’adopter ou non :

  • Cypress s’exécute seulement dans le navigateur, en mode headless ou headed
  • Il n’est pas possible d’exécuter des tests dans plusieurs onglets du navigateur à la fois
  • Cypress ne peut pas contrôler plus d’un navigateur à la fois
  • Dans un même test, il n’est pas possible de visiter deux URLs avec des same-origin policy différents

Certaines de ces limitations sont permanentes, c’est-à-dire que la nature et le fonctionnement même de Cypress ne lui permettent pas de les outrepasser. D’autres peuvent être contournées.

Mais si vous vous retrouvez à chercher des workaround pour toutes les limitations de Cypress, alors il y a des chances que l’outil ne soit pas adapté à vos besoins.

Un petit cas pratique

Maintenant qu’on a fait un tour d’horizon sur Cypress et son fonctionnement, essayons de voir comment il se comporte en situation réelle.

L’application que nous allons tester

L’application qu’on va utiliser pour nos tests, c’est TodoMVC. Comme vous le devinez, c’est une application de to-do liste. Elle a la particularité d’être écrite dans plusieurs langages/technologies JavaScript. Le but des auteurs, c’est de permettre de permettre aux développeur de voir l’implémentation d’une application JavaScript complète dans plusieurs cas d’usages.

Page d'accueil Todo-MVC
Page d’accueil TodoMVC

En termes de fonctionnement, l’application permet de créer des éléments de to-do, de les marquer comme achevés, terminés, etc.

Fonctionnement de l’application Todo-MVC

Pour ce petit cas pratique, nous allons utiliser la version de l’application en JavaScript natif (vanilla JS). Dans notre contexte, ça ne change pas grand-chose d’utiliser une version plutôt qu’une autre, parce que nous n’allons tester que le frontend de l’application, directement dans le navigateur.

Débuter avec Cypress

Je ne vais pas m’attarder sur l’installation de Cypress, parce que la documentation officielle couvre très bien cet aspect (et beaucoup d’autres d’ailleurs).

Ce qu’il faut retenir, c’est que Cypress est une application desktop, avec des supports pour macOS, Linux et Windows. C’est également possible de l’utiliser comme package npm. Tous les détails sur l’installation et les prérequis sont ici.

Les fonctionnements de l’application que nous allons tester

Ici, nous allons tester les fonctionnalités de base de Todo-MVC. Comme l’ajout, la suppression de tâches, le classement correct des tâches selon qu’elles sont terminées ou actives, et les informations relatives au nombre de tâches dans les différentes vues.

Autre détail, je ne vais pas cloner le dépôt de l’application dans mes tests, parce que je ne ferai pas d’interaction directe avec le code, et que l’application est directement accessible en ligne.

Poser les bases des tests

Écrire les scénarios des tests

Avant de se mettre même à écrire un test, l’une des premières étapes est d’écrire les scénarios des tests. C’est très important de le faire, parce que cela permet d’avoir une vue d’ensemble sur tous les tests à écrits. Aussi, en écrivant les scénarios des tests, on a l’occasion de parcourir le fonctionnement de l’application, et on peut commencer à se faire une idée du code à écrire.

Selon les contextes, les technologies utilisées, etc. la manière d’écrire les scénarios peut varier. En règle générale, l’important est de les rendre le plus lisible possible.

Pour les tests que je vais écrire, voilà à quoi ressemble mes scénarios, qui sont juste dans un simple fichier .txt.

scenarios.txt
-----------------------------------------------------------------------------------

--- Adding a new task ---
Given that the Todo-MVC app is running
When I add a new task
Then the new task should appear in the task list
And the task number should be 1

--- Showing a new task as active ---
Given that the Todo-MVC app is running
When I add a new task
And I click the active tasks link
Then the new task should appear in the active tasks list

--- Deleting a task ---
Given that the Todo-MVC app is running
When I add a new task
And I click the delete button for that task
Then the task should be removed from the task list
And the to-do list <ul> tag should be removed

--- Marking a task as complete ---
Given that the Todo-MVC app is running
When I add a new task
And I click the complete toggle button for that task
Then the task label should be crossed out
And the complete tasks list should contain the task

--- Marking all tasks as complete ---
Given that the Todo-MVC app is running
When I add two new tasks
And I click the toggle all button
Then the task labels should be crossed out
And the complete tasks list should contain both tasks

--- Removing completed tasks ---
Given that the Todo-MVC app is running
When I add two new tasks
And I click the toggle all button
And I click the "Clear completed" button
Then the completed tasks should be removed from the task list
And the to-do list <ul> tag should be removed

Ils sont en anglais juste parce que trouve cela plus confortable.

À quoi ressemble Cypress quand on l’installe ?

Architecture de fichiers Cypress
Architecture de fichiers Cypress

Quand Cypress est installé dans un projet, voilà à quoi ressemble l’architecture des fichiers.

  • Le fichier cypress.json contient toutes les configurations des tests, comme celles relatives à l’environnement, l’exécution des tests, etc.
  • Le dossier fixtures contient les données provenant de sources externes et qui peuvent être utilisées dans les tests. Dans notre cas, il contient juste un fichier task.json avec les titres des tâches qui vont être créées durant les tests.
  • Le dossier integration contient le code même des tests. Ce dossier peut être bien évidemment structuré pour plus de clarté dans le code. Il peut aussi être changé ou configuré pour que par exemple, les fichiers avec une extension en particulier soient reconnus comme des tests par Cypress.
  • Le dossier plugins contient toutes les extensions qui viendront modifier les comportements internes de Cypress
  • Le dossier support va contenir tout le code custom qu’on peut écrire, et qui va être réutilisé dans les tests.

Écrire les premiers tests

À cette étape, on peut commencer à écrire nos premiers tests. D’après nos scénarios, on va donc commencer par tester la création d’une tâche et la vérification qu’une nouvelle tâche est bien libellée comme active.

!

Je ne vais pas m’étendre sur la syntaxe pour les tests avec Cypress (qui reste très très lisible), parce que je pense qu’on la comprend mieux en faisant ; et la documentation officielle présente de manière très claire comment démarrer.

Cela dit, voilà à quoi ressemble mon code initial pour les deux premiers tests.

integration/0-lurking/sample_specs.js
-----------------------------------------------------------------------------------

describe('Test todomvc.com frontend output', () => {
    it('correctly adds a new task to the task list', function() {
        cy.visit('https://todomvc.com/examples/vanillajs/');
        cy.get('.new-todo').type(`Test task 1{enter}`);
        cy.get('ul.todo-list label').should('contain', 'Test task 1');
        cy.get('span.todo-count').should('contain', '1 item left');
    })

    it('correctly shows new task as active', function() {
        cy.visit('https://todomvc.com/examples/vanillajs/');
        cy.get('.new-todo').type(`Test task 1{enter}`);
        cy.get('a').contains('Active').click();
        cy.get('ul.todo-list label').should('contain', 'Test task 1');
    })
})

Et voilà le résultat des tests dans l’exécutant de tests.

Exécutant de tests Cypress

💡

Pour lancer l’exécution des tests, j’ai juste à faire npx cypress open dans la console.

Comme on peut le voir, les deux premiers tests passent bien (et très vite). Le test runner de Cypress fournit pleins d’informations utiles sur l’exécution des tests, et ça, c’est vraiment génial comme expérience.

Mais si on regarde mon code initial plus attentivement, on remarque qu’il y a des étapes qui sont répétées dans les deux tests :

  • Visiter l’URL de l’application
  • Créer une nouvelle tâche

Donc ces deux lignes :

integration/0-lurking/sample_specs.js
-----------------------------------------------------------------------------------

cy.visit('https://todomvc.com/examples/vanillajs/');
cy.get('.new-todo').type(`Test task 1{enter}`);

On peut aisément anticiper que ces deux actions vont se répéter dans tous les tests, donc c’est une bonne idée d’en faire une abstraction qu’on va réutiliser partout.

Pour cela, on va utiliser le hook beforeEach() de Mocha, qui rappelé vous, est embarqué dans Cypress. Et on va aussi mettre à profit les données que nous avons dans le dossier fixtures.

fixtures/tasks.json
-----------------------------------------------------------------------------------

{
  "task1": "Test task 1",
  "task2": "Test task 2"
}

On va donc dans notre beforeEach() importer une tâche, visiter la page de l’application et créer la tâche. Notre code va donc ressembler à quelque chose dans le style.

integration/0-lurking/sample_specs.js
-----------------------------------------------------------------------------------

describe('Test todomvc.com frontend output', () => {
    beforeEach(function() {
        cy.fixture('task').then(task => {
            this.task = task;
            cy.visit('https://todomvc.com/examples/vanillajs/');
            cy.get('.new-todo').type(`${task["task1"]}{enter}`);
        });
    });

    it('correctly adds a new task to the task list', function() {
        cy.get('ul.todo-list label').should('contain', this.task["task1"]);;
        cy.get('span.todo-count').should('contain', '1 item left');
    })

    it('correctly shows new task as active', function() {
        cy.get('a').contains('Active').click();
        cy.get('ul.todo-list label').should('contain', this.task["task1"]);;
    })
})

Cela va grandement nous faciliter les choses pour les tests qui vont suivre. Dans ce code, j’utilise la commande cy.fixture() de Cypress pour charger le fichier .json dont j’utilise le contenu pour créer la tâche.

🤔

J’imagine que ça va être pertinent plus tard de ne charger ce fichier qu’une seule fois. Le code précédent s’en sort bien avec deux lignes de JSON et une dizaine de tests, mais, avec plus de données, c’est définitivement un no-go.

Itérer et implémenter les scénarios restants

Dès qu’on a une base solide avec les premiers tests, on peut se mettre à itérer, améliorer le code et écrire le reste des tests.

Voilà à quoi mon code ressemble en rajoutant rapidement les tests qui suivent.

integration/0-lurking/sample_specs.js
-----------------------------------------------------------------------------------

describe('Test todomvc.com frontend output', () => {
    beforeEach(function() {
        cy.fixture('task').then(task => {
            this.task = task;
            cy.visit('https://todomvc.com/examples/vanillajs/');
            cy.get('.new-todo').type(`${task["task1"]}{enter}`);
        });
    });

    it('correctly adds a new task to the task list', function() {
        cy.get('ul.todo-list label').should('contain', this.task["task1"]);;
        cy.get('span.todo-count').should('contain', '1 item left');
    })

    it('correctly shows new task as active', function() {
        cy.get('a').contains('Active').click();
        cy.get('ul.todo-list label').should('contain', this.task["task1"]);;
    })

    it('correctly delete from the view a task ' +
        'on a click on the delete button',
        function() {
            cy.get('button.destroy')
                .invoke('show')
                .click();
            cy.get('ul.todo-list').should('not.be.visible');
        })

    it('correctly mark a task as completed ' +
        'on a click on the completion checkbox',
        function() {
            cy.get('input.toggle').check();
            cy.get('li.completed label')
                .should('have.css', 'text-decoration', 'line-through solid rgb(217, 217, 217)');
            cy.get('span.todo-count').should('contain', '0 items left');
            cy.get('a').contains('Completed').click();
            cy.get('ul.todo-list label').should('contain', this.task["task1"]);
        })

    it('correctly mark all task as completed on a click on all completion toggle', function() {
        cy.get('.new-todo').type(`${this.task["task2"]}{enter}`);
        cy.get('label[for="toggle-all"]').click();
        cy.get('li.completed label')
            .should('have.css', 'text-decoration', 'line-through solid rgb(217, 217, 217)');
    });

    it('correctly removes completed tasks on a click on "Clear completed"', function() {
        cy.get('label[for="toggle-all"]').click();
        cy.get('button').contains('Clear completed').click();
        cy.get('ul.todo-list').should('not.be.visible');
    })
})

Et voilà les résultats du test runner.

Exécutant de tests Cypress

J’ai téléversé tout le code des tests dans ce dépôt GitHub pour un accès plus facile.

Que retenir ?

  • Cypress peut s’avérer être un outil très intéressant quand il est utilisé dans le bon contexte
  • Il est important d’analyser correctement les limitations de l’outil avant de l’adopter
  • IL EST IMPORTANT DE LIRE LES DOCUMENTATIONS
  • Les scénarios de tests sont primordiaux quand on veut implémenter des tests de bout en bout (e2e tests)
  • Il est plus facile d’implémenter une suite de tests stable et efficace quand on commence par les bases, et qu’on itère régulièrement

À la découverte des tests de bout en bout

Les tests de bout en bout, c’est quoi déjà ?

Problème

Avez-vous déjà essayé de tester le fonctionnement d’une application ou site web que vous avez créé ? Imaginez-vous par exemple devoir aller sur chaque page de votre application et tester manuellement chacune des fonctionnalités. Un clic sur un bouton, l’ajout de contenu, la suppression d’un contenu, l’insertion de données, etc. Pas très enchantant non ?

Maintenant imaginez que vous devez faire cela à chaque nouvelle version de votre application. Ça devient très vite embêtant n’est-ce pas ? Allons un peu plus loin et supposons que vous devez faire cela à chaque fois qu’une nouvelle fonctionnalité est incluse dans l’application ; pour être sûr qu’elle n’interfère pas d’une manière non voulue avec une autre.

Vous devriez commencer déjà à percevoir les limites d’une telle méthode.

Définition

Les tests de bout en bout (end to end testing ou e2e testing en anglais) viennent proposer une manière plus automatisée et efficace de tester le fonctionnement d’une application. La philosophie de ces tests est de simuler un parcours utilisateur sur une application, du début à la fin.

L’idée ici, c’est de recréer des conditions similaires à celles qu’un réel utilisateur rencontrerait en utilisant l’application. Ensuite, de simuler son parcours, puis de rapporter les différents éléments qui ne fonctionnent pas comme prévu.

Méthodologie

Bien que le fonctionnement des tests de bout en bout semble tout banal, il y a une méthodologie à suivre pour les mettre en place afin de les rendre efficaces.

Analyse du fonctionnement de l’application

Pour tester de bout en bout une application, il faut avant savoir comment elle fonctionne, et à quoi s’attendre quand on effectue tel ou tel test.

Par exemple, supposons qu’on veuille tester une application ou un site web comme Google. Quand on est sur la page d’accueil du moteur de recherche et qu’on écrit une expression à rechercher, on est renvoyé sur une nouvelle page avec les résultats de la recherche effectuée.

Voilà un comportement qu’on doit connaître au préalable si la recherche depuis la page d’accueil est une fonctionnalité que l’on veut tester. On espère être renvoyé sur une nouvelle page avec les résultats de la recherche effectuée.

Vous devinez aisément que plus une application a de fonctionnalités, plus les scénarios de tests sont nombreux. C’est donc important de les identifier avant d’écrire tout test.

Une fois les scénarios de tests identifiés, il faut les formuler de manière à ce qu’ils aient un sens et puissent être retranscrits en code. Dans notre exemple avec Google, notre scénario de test peut être le suivant :

  1. J’ouvre mon navigateur
  2. Je vais sur la page d’accueil de Google
  3. J’entre mon expression à rechercher dans la barre de recherche (On n’a pas besoin de cliquer au préalable dans la barre de recherche parce que Google le fait automatiquement)
  4. J’appuie sur la touche Entrée
  5. J’espère voir une page avec une liste de résultats de recherche

Il faut ensuite refaire ce processus et écrire les scénarios pour toutes les autres fonctionnalités que nous voulons tester.

Mise en place d’un environnement de test

Une fois les scénarios de tests recensés et écrits, il faut penser ensuite à l’environnement dans lequel les tests vont être exécutés. En général, cet environnement est un navigateur, dans lequel les scénarios vont être simulés. Il faut bien sûr prendre en compte tous les prérequis pour que l’application à tester fonctionne parfaitement dans cet environnement.

A t’on besoin de se connecter à un compte avant d’utiliser l’application ? Certaines données sont elles nécessaires pour tester l’application. Où et comment ces données sont-elles créées ? Comment sont-elles stockées ?

Voilà des questions que l’on peut se poser pendant la mise en place de l’environnement de test ; qui se doit d’être le plus proche possible sinon exact à l’environnement dans lequel un utilisateur réel pourrait se retrouver.

Exécuter et automatiser les tests, reporter les erreurs, améliorer l’application

L’objectif ultime des tests de bout en bout est de s’assurer du bon fonctionnement de l’application, mais aussi d’identifier les comportements prévus de l’application qui ne fonctionnement pas, et y apporter des solutions.

Il faut donc exécuter nos tests, mais aussi automatiser autant que possible ce processus d’exécution. C’est là que ça commence à devenir intéressant. Une fois les tests écrits, vérifiés, on peut maintenant les exécuter automatiquement à des intervalles donnés. Chaque jour, chaque semaine, à chaque fois qu’une nouvelle fonctionnalité est ajoutée, etc.

Ensuite, il faut à côté de ce processus d’automatisation, mettre en place un système de rapports pour avoir une vue d’ensemble sur comment les tests fonctionnent, d’où surviennent les erreurs, etc. Tout ça afin d’identifier au plus vite les éventuels problèmes et les régler.

C’est beau tout ça, mais comment on fait concrètement pour créer des tests de bout en bout ?

En effet, maintenant qu’on sait ce que c’est que les tests de bout en bout, comment ils fonctionnent, on se demande bien comment les créer.

L’univers des tests e2e est très grand. C’est même une discipline à part entière. Il y a donc de nombreuses technologies qui permettent de créer ces tests. Et à côté de nombreux services pour les accompagner, que ce soit pour l’automatisation, le monitoring, les rapports, etc.

Évidemment, le choix d’un ou de plusieurs outils va dépendre du type d’application à tester, des technologies utilisées dans l’application, etc.

Je vais présenter brièvement ici quelques technologies pour mettre en place un système de tests e2e, pour les automatiser et les rapporter. Ces technologies sont principalement celles de que j’ai utilisé dans le cadre de mon travail, ou personnellement pour en apprendre plus sur le sujet.

Jest

Jest est un exécutant de test JavaScript créé et maintenu par Facebook, qui couvre plusieurs aspects des tests d’application, dont les tests de bout en bout. C’est un framework pour projets JavaScript, et il est connu pour être très flexible. Le framework embarque aussi une librairie d’assertion très puissante ; et s’intègre très bien avec la librairie Puppeteer.

J’utilise régulièrement depuis quelques mois dans la suite de tests de bout en bout de WordPress.

C’est également l’outil que j’ai utilisé pour créer la suite de tests de bout en bout de Yoast SEO, l’extension de SEO par excellence pour WordPress.

L’une des choses que j’aime chez ce framework, c’est sa courbe d’apprentissage qui n’est pas très élevée.

CodeceptJS

CodeceptJS est un autre framework JavaScript qui est quant à lui spécifique pour les tests de bout en bout. Il a l’avantage d’embarquer de nombreuses fonctionnalités avancées qui permettent d’interagir avec une application dans un environnement de test.

Autre gros avantage de ce framework, il permet nativement d’utiliser plusieurs outils de contrôle de navigateur, comme Puppeteer, Selenium, Playwright ou encore TestCafe.
Je l’utilise aussi régulièrement sur des projets internes à Yoast.

Cypress

Cypress est plutôt nouveau dans la sphère des outils de tests d’application. Je l’ai d’ailleurs découvert pendant mes recherches pour cet article. En fait, j’ai tout de suite aimé cet outil quand j’ai commencé à en apprendre plus. En termes de philosophie, les créateurs ont pour ambition de faciliter et de rendre accessible l’implémentation des tests automatisés.

Et ça se remarque très vite quand on commence à l’utiliser. Le test runner est très rapidement pris en main et les interfaces d’exécution des tests sont très épurés.
Cypress embarque aussi nativement Chai, la très polyvalente librairie d’assertion pour Node.

Bien sûr, il y a beaucoup d’autres outils qui peuvent être utilisés pour les tests automatisés. Que ce soit des outils de contrôle de navigateur, des librairies/modules d’assertion ou encore des framework tout en un. Encore mieux, il est régulièrement possible d’intégrer ces outils entre eux, afin de tirer profit de chacun de leurs avantages.