Node.js e2e: Testez votre application de gestion d'offres commerciales avec TestCafé (partie annexe)

Node.js e2e: Testez votre application de gestion d'offres commerciales avec TestCafé (partie annexe)
Categories:
Posté le

Le test applicatif / logiciel est un sujet récurrent lors des développements et notamment dans une optique d'un déploiement continu avec l'ajout de nouvelles fonctionnalités.

Votre adaption aux besoins clients par l'ajout de nouvelles fonctionnalités ou de features répondant aux besoins est un élément-clef de la réussite de votre business et notamment dans la distribution de solutions applicatives (SaaS).

Se pose alors un problème, comment assurer le product market fit et donc l'adaptation de vos solutions à l'évolution du marché tout en assurant un besoin crucial de qualité et de stabilité ?

L'ajout d'une nouvelle fonctionnalité qui aurait pour effet de rendre d'anciennes inutilisables/instables et donc d’entraîner une régression applicative aurait des conséquences non seulement business mais également sur l'image donnée par la solution.

Il est alors primordial d'appliquer des politiques de tests de régression afin d'assurer la stabilité applicative mais également de continuer à livrer de nouvelles fonctionnalités régulièrement.

C'est là qu'interviennent les tests de bout en bout, terme qui annonce souvent des implémentations longues et demandeuses en configurations. Mais il existe des solutions pour rendre l'implémentation de ces tests plus aisés.

Nous allons voir ici comment grâce à TestCafé et notre application de gestion de vos offres commerciales (qui servira d’exemple) comment intégrer facilement ces tests de bout en bout, appelés plus couramment tests End-to-End ou encore E2E.

Les technologies utilisées :

Les sources de l'article : https://github.com/Zenicheck/blog/tree/master/active-offer-koa-test

Mise en place d'environnement de test :

Il vous faut tout d'abord installer TestCafé :

$ npm install -g testcafe

Et lancer l'application qui servira de test (en ayant bien sûr configuré les différentes variables d'environnement) :

$ more run.sh
#!/bin/sh

PORT=8900 npx nodemon --experimental-modules --experimental-json-modules bin/www
.mjs --watch routes --watch views --watch helpers --watch app.mjs
$ ./run.sh
[nodemon] starting `node --experimental-modules --experimental-json-modules bin/www.mjs`

L'application devrait maintenant être accessible sur le port 8900 : http://localhost:8900/so

Dans un dossier nommé test créer le fichier de configuration de TestCafé :

/* config.js */
export default {
    baseUrl: "http://localhost:8900"
}

Écriture des tests pour l'authentification utilisateur :

Une série de tests de TestCafé commence toujours par la création d'une fixture qui va décrire le nom de la série de tests mais également, le plus important, l'url sur laquelle démarre celle-ci.

Pour rappel les tests de bout en bout sont des tests permettant de simuler les actions d'un utilisateur (ici à travers un navigateur) afin de voir si l’ensemble applicatif réagit comme attendu.

À la fin de l'écriture des différents tests nous obtiendrons donc une simulation suivante (tests d'authentification puis du CRUD des offres):

Retournons donc sur l'implémentation des tests d'authentification qui se découperont donc comme ceci :

  • Redirection en cas de non authentification
  • Test de la présence du formulaire
  • Test d'affichage de messages d'erreur
  • Test de connexion

On a donc le fichier suivant (Il faut évidemment remplacer le login/ mot de passe par celui configuré dans votre application) :

/* Auth.js */
import config from './config'
import { Selector } from 'testcafe'

fixture`Login`
    .page`${config.baseUrl}/adm`

test('Logout', async t => {
    await t
        .navigateTo(`${config.baseUrl}/so/logout`)
})

test('Redirect Check title', async t => {
    await t
        .expect(Selector('title').innerText).eql('Gestion de vos offres commerciales - Se connecter')
})

test('Login Form', async t => {
    const loginBtn = Selector('button').withAttribute('type', 'submit')
    const form = Selector('form')
    await t
        .expect(form.count).eql(1)
        .expect(loginBtn.count).eql(1)
        .expect(loginBtn.nth(0).textContent).contains('Se connecter')
})

test('Login Fields', async t => {
    const emailField = Selector('input#soEmail')
    const passwordField = Selector('input#soPassword')
    await t
        .expect(emailField.exists).ok()
        .expect(passwordField.exists).ok()
})

test('Login Error Message', async t => {
    const emailField = Selector('input#soEmail')
    const passwordField = Selector('input#soPassword')
    const loginBtn = Selector('button').withAttribute('type', 'submit').nth(0)
    const errorToast = Selector('.bg-danger > .card-body')
    await t
        .typeText(emailField, 'contact@yanncarlen.com')
        .typeText(passwordField, '1234')
        .expect(emailField.exists).ok()
        .expect(passwordField.exists).ok()
        .click(loginBtn)
        .expect(errorToast.count).eql(1)
        .expect(errorToast.nth(0).textContent).contains('Email ou Mot de Passe invalide')
})

test('Login Success', async t => {
    const emailField = Selector('input#soEmail')
    const passwordField = Selector('input#soPassword')
    const loginBtn = Selector('button').withAttribute('type', 'submit').nth(0)
    await t
        .typeText(emailField, 'contact@yanncarlen.com')
        .typeText(passwordField, '_admin')
        .expect(emailField.exists).ok()
        .expect(passwordField.exists).ok()
        .click(loginBtn)
        .expect(Selector('title').innerText).eql('Gestion de vos offres commerciales - Adm')
})

L'api de TestCafé est assez littérale et permet de comprendre aisément ce que font les différentes fonctions.

On a donc des assertions qui vérifient la présence d'éléments sur la page, notamment avec la fonction count mais également des actions comme click

Pour lancer la série de tests il faut lancer TestCafé de la manière suivante :

$  testcafe firefox Auth.js

Si vous n'avez pas Firefox d'installer les différents options de navigateurs sont les suivantes : https://devexpress.github.io/testcafe/documentation/using-testcafe/common-concepts/browsers/browser-support.html#locally-installed-browsers

Implémentation des tests sur le CRUD des offres commerciales :

Afin d'implémenter les tests sur le crud des offres il faut utiliser un concept de TestCafé appelé Roles (User Roles). Ils vont permettre avant chaque action de définir comment un utilisateur doit s'authentifier, les sessions étant vidées entre chaque test afin d'éviter les effets de bord.

On a donc le rôle suivant :

/* Offer.js */
import config from './config'
import { Role, Selector } from 'testcafe'

fixture`Offer`
    .page`${config.baseUrl}/adm/offer`

const regularAccUser = Role(`${config.baseUrl}/so`, async t => {
    const loginBtn = Selector('button').withAttribute('type', 'submit')
    await t
        .typeText('#soEmail', 'contact@yanncarlen.com')
        .typeText('#soPassword', '_admin')
        .click(loginBtn.nth(0))
})

Il faut là encore remplacer les informations de connexion avec celles de votre configuration.

Puis les tests du CRUD avec le schéma suivant :

  • Accès au dashboard
  • Accès à l'action d'ajout
  • Messages d'erreur sur le formulaire d'ajout
  • Ajout d'une offre
  • Présence de l'offre ajoutée dans la datatables des offres
  • Suppression d'une offre
  • Vérification suppression avec message flash

L'implémentation des tests :

/*
** Offer.js
*/
/*
** ...
*/

test('Adm Dashboard', async t => {
    await t
        .useRole(regularAccUser)
        .expect(Selector('title').innerText).eql('Gestion de vos offres commerciales - Adm')
})

test('Offer Dashboard', async t => {
    await t
        .useRole(regularAccUser)
        .navigateTo(`${config.baseUrl}/adm/offer`)
        .expect(Selector('title').innerText).eql('Gestion de vos offres commerciales - Listes des offres')
})

test('Add Offer Button', async t => {
    const addBtn = Selector('a').withAttribute('href', '/adm/offer/add')
    await t
        .useRole(regularAccUser)
        .expect(addBtn.count).eql(1)
        .expect(addBtn.nth(0).exists).ok()
})

test('Add Action', async t => {
    const addBtn = Selector('a').withAttribute('href', '/adm/offer/add').nth(0)

    await t
        .useRole(regularAccUser)
        .click(addBtn)
        .expect(Selector('title').innerText).contains('Ajouter')
})

test('Add Form', async t => {
    const addActionBtn = Selector('a').withAttribute('href', '/adm/offer/add')
    const addBtn = Selector('button').withAttribute('type', 'submit')
    const titleBtn = Selector('input#offerTitle')
    const urlBtn = Selector('input#offerLink')

    await t
        .useRole(regularAccUser)
        .click(addActionBtn)
        .expect(addBtn.count).eql(1)
        .expect(addBtn.nth(0).exists).ok()
        .expect(titleBtn.exists).ok()
        .expect(urlBtn.exists).ok()
})

test('Add Form Error Messages', async t => {
    const addActionBtn = Selector('a').withAttribute('href', '/adm/offer/add')
    const addBtn = Selector('button').withAttribute('type', 'submit')
    const titleBtn = Selector('input#offerTitle')
    const urlBtn = Selector('input#offerLink')
    const errorToast = Selector('.bg-danger > .card-body')

    await t
        .useRole(regularAccUser)
        .click(addActionBtn)
        .typeText(titleBtn, 'a')
        .typeText(urlBtn, 'https://zenicheck.com')
        .click(addBtn)
        .expect(errorToast.count).eql(1)
        .expect(errorToast.nth(0).textContent).contains('4 et 255 charactères')
})

test('Add Form Success', async t => {
    const addActionBtn = Selector('a').withAttribute('href', '/adm/offer/add')
    const addBtn = Selector('button').withAttribute('type', 'submit')
    const titleBtn = Selector('input#offerTitle')
    const urlBtn = Selector('input#offerLink')

    await t
        .useRole(regularAccUser)
        .click(addActionBtn)
        .typeText(titleBtn, 'TestCafe')
        .typeText(urlBtn, 'https://zenicheck.com')
        .click(addBtn)
        .expect(Selector('title').innerText).eql('Gestion de vos offres commerciales - Listes des offres')
})

test('Add Form Table Success', async t => {
    const offerBtn = Selector('a').withAttribute('href', '/adm/offer')
    const offerTable = Selector('table#offerTable tr')
    await t
        .useRole(regularAccUser)
        .click(offerBtn)
        .expect(offerTable.nth(-1).exists).ok()
        .expect(offerTable.nth(-1).find('td').nth(0).exists).ok()
        .expect(offerTable.nth(-1).find('td').nth(0).textContent).contains('TestCafe')
})

test('Delete Form', async t => {
    const offerBtn = Selector('a').withAttribute('href', '/adm/offer')
    const offerTable = Selector('table#offerTable tr')
    await t
        .useRole(regularAccUser)
        .click(offerBtn)
        .expect(offerTable.nth(-1).exists).ok()
        .expect(offerTable.nth(-1).find('td').nth(3).find('a').nth(2).textContent).contains('Supprimer')
        .click(offerTable.nth(-1).find('td').nth(3).find('a').nth(2))
        .expect(Selector('title').innerText).contains('Editer')
})

test('Delete Form Confirm', async t => {
    const offerBtn = Selector('a').withAttribute('href', '/adm/offer')
    const offerTable = Selector('table#offerTable tr')
    const deleteBtn = Selector('button').withAttribute('type', 'submit')
    await t
        .useRole(regularAccUser)
        .click(offerBtn)
        .expect(offerTable.nth(-1).exists).ok()
        .expect(offerTable.nth(-1).find('td').nth(3).find('a').nth(2).textContent).contains('Supprimer')
        .click(offerTable.nth(-1).find('td').nth(3).find('a').nth(2))
        .expect(deleteBtn.nth(0).exists).ok()
        .expect(deleteBtn.nth(0).textContent).contains('Supprimer')
        .click(deleteBtn.nth(0))
        .expect(Selector('.flash').count).eql(1)
        .expect(Selector('.flash').textContent).contains('Offre supprimée')
})

Dans la partie prochaine interviendra l'implémentation des fonctionnalités suivantes :

  • API REST pour l'accès public des offres
  • Exemple d'implémentation d'une iframe avec personnalisation du visuel pour l'API REST
  • Exemple d'utilisation de l'API sur un site de référence (exemple avec Bludit et Squarespace)
  • Déploiement de la plateforme avec Docker 
  • Complétion des tests e2e

Les sources de l'article : https://github.com/Zenicheck/blog/tree/master/active-offer-koa-test

Si vous souhaitez intégrer des tests lors de vos développements applicatifs que cela soit avec TestCafé, Selenium ou Nightwatch.js par exemple, le tout en les intégrants dans des outils d'intégration continue (Jenkins, ...), n'hésitez pas à me contacter !

Je serai ravis de vous accompagner afin d'augmenter votre fiabilité applicative.

Les autres articles de cette série :