La plupart des projets web et plus particulièrement leurs applications front sont souvent dénuées de tests fonctionnels voire même de tests unitaires par manque de temps ou par choix. Nous allons dans cet article vous présenter comment réaliser vos tests fonctionnels facilement et rapidement et ce quelque soit le framework front-end utilisé. Pour cet article nous allons partir d’un projet Angular issu d’un précédent article sur les tests unitaires avec Jest (https://blog.ineat-group.com/2019/04/angular-remplacer-karma-jasmine-par-jest/).

Le framework choisi pour cet article est Angular mais vous pouvez facilement intégrer Cypress à n’importe quelle application utilisant React, Vue ou même Vanilla. Seul l’importation de la librairie sera différente en fonction du framework ou librairie utilisé.

La plupart des frameworks ou librairies front récents embarquent ou conseillent leurs propres outils de tests fonctionnels ou end-to-end (e2e). C’est le cas d’Angular qui embarque le couple Karma/Protractor, de VueJS qui conseille Sélénium via Nightwatch. Dans notre cas, nous allons dans un premier temps remplacer le couple Karma/Protractor par Cypress puis dans un second temps nous verrons comment réaliser nos tests fonctionnels.

Récupération du projet

Avant de commencer, nous allons récupérer le projet d’exemple. ouvrez un terminal dans un dossier de votre choix puis cloner le projet Github :

git clone git@github.com:ineat/Angular-Jest-Tutorial.git ngCypress

Nous allons maintenant exécuter le projet afin de voir l’application que nous allons tester avec Cypress.

Depuis votre terminal, accédez au dossier ngCypress puis exécutez l’une des commandes suivantes pour installer les dépendances yarn install ou  npm install.

Puis pour exécutez le projet yarn start ou npm run start. Vous devez obtenir ceci dans votre terminal :

Ouvrez maintenant un navigateur et accédez à l’url suivante : http://localhost:4200 pour obtenir cette application :

Attention: il vous faut le CLI Angular installé sur votre poste pour lancer l’application.

Nous avons maintenant un projet Angular fonctionnel.

Remplacer Karma/Protractor par Cypress

Installation de Cypress

Pour installer Cypress, rien de plus simple :

ng add @cypress/schematic

Répondre Y à la question suivante:
Would you like the default `ng e2e` command to use Cypress? [ Protractor to Cypress Migration Guide: https://on.cypress.io/protr
actor-to-cypress?cli=true ]

Normalement nous pouvons désormais exécuter nos tests fonctionnels avec Cypress en exécutant le commande yarn e2e ou npm run e2e (Attention à ne pas avoir de serveur de développement qui tourne sur le port 4200). Mais avant de lancer les tests, nous allons écrire un test simple, nous allons vérifier que le footer de l’application contient bien le text “© Ineat Group – iLab Team.”.

Pour cela, nous allons supprimer le contenu du dossier cypress/integration puis nous allons créer un fichier hero-team-selectror.spec.ts avec le code suivant :

describe("Hero Team Selector", () => {
  beforeEach(() => {
    cy.visit("http://localhost:4200");
  });

  it("Application footer contain copyright text", () => {
    cy.get(".app--footer")
      .find("p")
      .should("contain.text", "© Ineat Group - iLab Team.");
  });
});

Une fois le fichier créé, nous pouvons exécuter le test fonctionnel en executant le commande yarn e2e ou npm run e2e .

L’ application Cypress s’ouvre et vous propose un seul fichier de test : hero-team-selector.spec.js.

Nous pouvons maintenant cliquer sur le fichier de tests pour l’exécuter ou appuyer sur le bouton “Run all specs”. Nous obtenons alors le résultat suivant :

Notre test fonctionne bien, si nous modifions le texte du footer dans le fichier : src/app/core/app.component.html on obtient un échec du test.

Annuler la modification. Voilà, nous avons une application que l’on peut tester avec Cypress. Nous allons maintenant créer nos propres tests fonctionnels.

Les tests fonctionnels avec Cypress

Avant d’écrire nos premiers test, on remarque que le screenshot du test est tronqué, nous allons régler ce problème en modifiant la configuration de Cypress via le fichier cypress.json

{
  "viewportWidth": 1280,
  "viewportHeight": 1024
}

On a maintenant un screenshot complet, nous pouvons rédiger nos premiers tests.

La home affiche bien une liste de héros, une liste de méchants et deux équipes vides

Pour le vérifier, nous allons ajouter trois nouveaux tests dans le fichier hero-team-selector.spec.js.

Pour s’affranchir des appels API et de la latence réseau qu’ils engendrent, nous allons mocker les appels api en ajoutant un mock dans la fonction beforeEach

...
beforeEach(() => {
    cy.intercept('/hero', { fixture: 'hero.json' });
    cy.intercept('/wicked', { fixture: 'wicked.json' });
    cy.visit("http://localhost:4200");
 });
...

La méthode cy.intercept va simuler le retour de l’appel api et renvoyer les données grâce aux fixtures présent sous forme de deux fichiers json déposés dans le dossier cypress/fixtures.

Il faut donc créer les deux fichiers de fixtures suivant :

Le fichier hero.json :

[
  {
    "id": 1,
    "name": "Deadpool",
    "real_name": "Wade Wilson",
    "thumb": "https://i.annihil.us/u/prod/marvel/i/mg/9/90/5261a86cacb99/standard_xlarge.jpg",
    "image": "https://i.annihil.us/u/prod/marvel/i/mg/9/90/5261a86cacb99.jpg",
    "description": ""
  },
  {
    "id": 2,
    "name": "Hulk",
    "real_name": "Robert Bruce Banner",
    "thumb": "https://i.annihil.us/u/prod/marvel/i/mg/5/a0/538615ca33ab0/standard_xlarge.jpg",
    "image": "https://i.annihil.us/u/prod/marvel/i/mg/e/e0/537bafa34baa9.jpg",
    "description": "Caught in a gamma bomb explosion while trying to save the life of a teenager, Dr. Bruce Banner was transformed into the incredibly powerful creature called the Hulk. An all too often misunderstood hero, the angrier the Hulk gets, the stronger the Hulk gets."
  },
  {
    "id": 3,
    "name": "IronMan",
    "real_name": "Anthony Edward \"Tony\" Stark",
    "thumb": "https://i.annihil.us/u/prod/marvel/i/mg/6/a0/55b6a25e654e6/standard_xlarge.jpg",
    "image": "https://i.annihil.us/u/prod/marvel/i/mg/c/60/55b6a28ef24fa.jpg",
    "description": "Inventor Tony Stark applies his genius for high-tech solutions to problems as Iron Man, the armored Avenger."
  },
  {
    "id": 4,
    "name": "Spider-Man",
    "real_name": "Peter Parker",
    "thumb": "https://i.annihil.us/u/prod/marvel/i/mg/3/50/526548a343e4b/standard_xlarge.jpg",
    "image": "https://i.annihil.us/u/prod/marvel/i/mg/3/50/526548a343e4b.jpg",
    "description": "Bitten by a radioactive spider, high school student Peter Parker gained the speed, strength and powers of a spider. Adopting the name Spider-Man, Peter hoped to start a career using his new abilities. Taught that with great power comes great responsibility, Spidey has vowed to use his powers to help people."
  },
  {
    "id": 5,
    "name": "Wolverine",
    "real_name": "James Howlett",
    "thumb": "https://i.annihil.us/u/prod/marvel/i/mg/2/60/537bcaef0f6cf/standard_xlarge.jpg",
    "image": "https://i.annihil.us/u/prod/marvel/i/mg/2/60/537bcaef0f6cf.jpg",
    "description": "Born with super-human senses and the power to heal from almost any wound, Wolverine was captured by a secret Canadian organization and given an unbreakable skeleton and claws. Treated like an animal, it took years for him to control himself. Now, he's a premiere member of both the X-Men and the Avengers."
  },
  {
    "id": 6,
    "name": "Black Panther",
    "real_name": "James Howlett",
    "thumb": "http://i.annihil.us/u/prod/marvel/i/mg/6/60/5261a80a67e7d/standard_xlarge.jpg",
    "image": "http://i.annihil.us/u/prod/marvel/i/mg/6/60/5261a80a67e7d.jpg",
    "description": "As the king of the African nation of Wakanda, T’Challa protects his people as the latest in a legacy line of Black Panther warriors."
  }
]

et le fichier wicked.json :

[
  {
    "id": 1,
    "name": "Loki",
    "real_name": "Loki",
    "thumb": "http://i.annihil.us/u/prod/marvel/i/mg/d/90/526547f509313/standard_xlarge.jpg",
    "image": "http://i.annihil.us/u/prod/marvel/i/mg/d/90/526547f509313.jpg",
    "description": ""
  },
  {
    "id": 2,
    "name": "Thanos",
    "real_name": "Thanos",
    "thumb": "http://i.annihil.us/u/prod/marvel/i/mg/6/40/5274137e3e2cd/standard_xlarge.jpg",
    "image": "http://i.annihil.us/u/prod/marvel/i/mg/6/40/5274137e3e2cd.jpg",
    "description": "The Mad Titan Thanos, a melancholy, brooding individual, consumed with the concept of death, sought out personal power and increased strength, endowing himself with cybernetic implants until he became more powerful than any of his brethren."
  },
  {
    "id": 3,
    "name": "Ultron",
    "real_name": "Ultron",
    "thumb": "http://i.annihil.us/u/prod/marvel/i/mg/3/70/5261a838e93c0/standard_xlarge.jpg",
    "image": "http://i.annihil.us/u/prod/marvel/i/mg/3/70/5261a838e93c0.jpg",
    "description": "Arguably the greatest and certainly the most horrific creation of scientific genius Dr. Henry Pym, Ultron is a criminally insane rogue sentient robot dedicated to conquest and the extermination of humanity."
  }
]

La home affiche une liste de 6 héros :

...
it("Available Hero List contains 6 members", () => {
    cy.get(".selector-wrapper")
      .find(".list")
      .eq(0)
      .find("app-hero-list-item")
      .should("have.length", 6);
 });
...

Arrêtons nous sur ce premier test pour décortiquer un peu l’API de Cypress :

...
  beforeEach(() => {
    cy.visit("http://localhost:4200");
  });
...

Dans le beforeEach on exécute des appels avant chaque test, dans notre cas on se rend sur la page http://localhost:4200 via la commande cy.visit.

Puis dans notre test :

...  
  it("Available Hero List contains 6 members", () => {
    cy.get(".selector-wrapper")
      .find(".list")
      .eq(0)
      .find("app-hero-list-item")
      .should("have.length", 6);
  });
...
  • cy.get permet de sélectionner l’élément du DOM comportant la class “selector-wrapper”
  • .find(“.list”) permet de rechercher tous les élément du DOM ayant la class “list” et présent dans l’élément “.selector-wrapper” et retourne une liste
  • .eq(0) sélectionne le premier de la liste
  • .find(“app-hero-list-item”) retourne la liste des éléments <app-hero-list-item>
  • .should(“have.length”, 6) vérifie si le nombre est égal à 6.

La home affiche une liste de 3 méchants :

...
  it("Available Wicked List contains 3 members", () => {
    cy.get(".selector-wrapper")
      .find(".list")
      .eq(1)
      .find("app-hero-list-item")
      .should("have.length", 3);
  });
...

La home affiche deux équipes vides :

...
  it("Home page contains 2 empty team", () => {
    cy.get(".teams-wrapper")
      .find(".team")
      .should("have.length", 2);
    cy.get(".teams-wrapper")
      .find(".team")
      .eq(0)
      .find("app-empty-card")
      .should("have.length", 5);
    cy.get(".teams-wrapper")
      .find(".team")
      .eq(1)
      .find("app-empty-card")
      .should("have.length", 5);
  });
...

Ajouter / Supprimer un héro d’une équipe

Ajouter un héro lorsqu’on clique sur le bouton “SELECT”

...
  it("Add Hero to Hero Team when select button was clicked", () => {
    cy.get(".selector-wrapper")
      .find(".list")
      .eq(0)
      .find("app-hero-list-item")
      .eq(0)
      .find("button")
      .click();
    cy.get(".teams-wrapper")
      .find(".team")
      .eq(0)
      .find("app-hero-card")
      .should("have.length", 1);
  });
...

Pour effectuer le clic sur un élément, on utilise la méthode .click() sur l’élément sélectionné.

Remplir une équipe et vérifier que l’équipe contient bien 5 membres

...
  it("Hero Team was full when 5 heroes were added", () => {
    for (let i = 0; i < 5; i++) {
      cy.get(".selector-wrapper")
        .find(".list")
        .eq(0)
        .find("app-hero-list-item")
        .eq(i)
        .find("button")
        .click();
    }

    cy.get(".teams-wrapper")
      .find(".team")
      .eq(0)
      .find("app-hero-card")
      .should("have.length", 5);
  });
...

On voit dans ce test que l’on peut créer des boucles pour ajouter plusieurs héros a une équipe de manière automatique. Puis on vérifie que l’équipe de héros contient bien 5 membres.

Supprimer un héro de l’équipe lors du clic sur bouton delete

...
  it("Remove Hero from Hero Team when remove button was cliked", () => {
    for (let i = 0; i < 3; i++) {
      cy.get(".selector-wrapper")
        .find(".list")
        .eq(0)
        .find("app-hero-list-item")
        .eq(i)
        .find("button")
        .click();
    }

    cy.get(".teams-wrapper")
      .find(".team")
      .eq(0)
      .find("app-hero-card")
      .should("have.length", 3);

    cy.get(".teams-wrapper")
      .find(".team")
      .eq(0)
      .find("app-hero-card")
      .eq(1)
      .find(".remove-btn")
      .click();

    cy.get(".teams-wrapper")
      .find(".team")
      .eq(0)
      .find("app-hero-card")
      .should("have.length", 2);
  });
...

Dans ce test, on commence par ajouter 3 héros à la liste avec la boucle for, puis on vérifie bien qu’on a bien nos 3 héros dans notre équipe. Une fois cette vérification faite, on va récupérer la team puis une des carte héro et on va effectuer le clic sur le bouton de suppression et nous allons ensuite vérifier que l’équipe contient non plus 3 mais 2 membres.

Conclusion

Voilà, nous avons tester les différentes actions utilisateurs de notre application. Les tests de cette application sont assez basiques mais Cypress permet de tester la quasi totalité de votre application web comme si un véritable utilisateur.

Il y a néanmoins quelques limitations comme la liste des navigateurs supportés par Cypress : en effet Cypress ne supporte que Chrome, Chromium, Firefox et Microsoft Edge, mais le support d’autres navigateurs (comme Safari) est toujours dans le backlog