Les xPath et les tableaux : 8 exercices pour se perfectionner

Il est vrai, rien ne surpasse les id dans les techniques d’identification des objets dans un DOM. C’est le plus simple et aussi le plus rapide à trouver par Selenium. Mais parfois (souvent) on ne peut pas faire autrement que d’écrire des xPath tellement complexes qu’ils frisent le « write-only ». Ces moments de solitude arrivent souvent lorsqu’on interagit avec des tableaux. On met 10 minutes à trouver la bonne formule et deux semaines après on ne s’en souvient plus. Nous voulons que ce temps soit révolu !

Voici donc 8 défis pour vous entraîner à écrire des xPath pour attraper des éléments dans des tableaux.

Les réponses se trouvent en bas de l’article.

Pour démarrer

Pour vérifier vos réponses nous vous conseillons l’outil CSS and XPath checker.

Ici, l’extension surligne tous les liens de la page. Pratique !

Toutefois, si vous ne souhaitez pas installer d’extension de navigateur (ou que vous n’avez pas le droit de le faire pour des raisons de sécurité !) il est tout à fait possible de se débrouiller autrement !

Sur votre navigateur, ouvrez l’inspecteur (touche F12 sur la plupart des navigateurs, ou « Option + Commande + I » sur Mac + Safari).

Ensuite, trouvez le bouton en forme de curseur, cliquez dessus, puis cliquer sur l’élément de la page dont vous souhaitez étudier le code source.

Exemples basiques

Pour écrire un xPath, nul besoin de retracer toute l’arborescence menant à l’élément en question ! Au contraire, plus votre xPath sera court et lisible, mieux ce sera pour la maintenance.

Voici quelques exemples d’xPath portant sur des liens (syntaxe « a » en HTML, en tant que diminutif de « anchor »).

  • Pour afficher tous les liens de la page, vous pouvez écrire « //a« 
  • Le troisième lien de la page sera trouvé avec « (//a)[3]« 
  • Le premier lien de chaque paragraphe sera trouvé avec « //p//a » (nul besoin de préciser [1] ; par défaut, c’est le premier résultat qui est pris en compte !)
  • Le premier lien dont le texte visible contient « automatisation » sera trouvé par « //a[contains(., ‘automatisation’)]« 
  • Le premier lien dont l’attribut @href (c’est à dire l’adresse vers laquelle il pointe, et qu’on peut trouver via l’inspecteur) contient « qualite-logicielle » sera trouvé par « //a[contains(@href, ‘qualite-logicielle’)]« 
  • Le premier lien dont l’attribut « id » est « connexion » sera trouvé par « //a[@id=’connexion’] » (pas besoin du mot-clé « contains » ici vu que l’id complet est mentionné dans le xPath !)

Ce ne sont là que des fondamentaux, l’exercice ci-dessous est un peu plus corsé ! 😁

L’exercice : xPath vs tableau HTML

Dans cet exercice, nous nous baserons sur ce tableau fruité. Vous pourrez valider vous-mêmes les formules sur la présente page.

Fruit Description Prix au kilo (XPF)
Pomme-liane Un feu d’artifice de saveurs acidulées 700
Banane poingo Le compromis entre la patate et la banane 500
Pomme-cannelle De loin, ressemble à un artichaut 630
Letchi Ce n’est plus la saison 700
Noix de coco Son germe est délicieux 350
Chouchoute Oups, ce n’est pas un fruit 300

Sous chaque question se trouve une copie d’écran du tableau avec le résultat attendu en surbrillance.

Exercices de xPath pour identifier des éléments dans le tableau

1) Ecrire un xPath pour récupérer la deuxième cellule qui contient « Pomme ».

2) Ecrire un xPath pour récupérer la première case des lignes dont la colonne 3 contient la chaîne 700.

3) Ecrire un xPath pour récupérer la deuxième case après les cellules contenant « Pomme-cannelle ».

4) Ecrire un xPath pour récupérer les cases qui contiennent des prix entre 300 (inclus) et 630 (exclu).

5) En hommage à Georges Perec, écrire un xPath pour récupérer les cases qui ne contiennent pas de « e » minuscule (cases de l’entête du tableau comprises).

6) Ecrire un xPath pour récupérer les lignes qui contiennent une case dont le texte commence par « Oups ».

7) Ecrire un xPath pour récupérer les cellules du tableau (hors entête) qui contiennent la chaîne « de », qu’elle soit en minuscules ou en majuscules.

8) Allez, un bien compliqué pour finir. Ecrire un xPath pour récupérer les noms des fruits dont la description contient le texte « est » et dont le prix est entre 300 (inclus) et 630 (exclu).

Alors, combien d’exercices avez-vous réussi à faire ? 😀

Rappel de bonnes pratiques

Attention, même s’il est préférable d’avoir des sélecteurs simples à comprendre, ne faites pas reposer la lisibilité de votre projet d’automatisation sur la lisibilité de vos sélecteurs. Envisagez l’adoption de l’objet Selecteur tel que nous détaillions dans notre article sur la lisibilité des logs Selenium :

public class Selecteur {
   public String nom;
   public By chemin;

   public Selecteur(String nom, By chemin){
      this.nom = nom;
      this.chemin = chemin;
   }

   public String getNom(){
      return nom;
   }

   public By getChemin(){
      return chemin;
   }
}

Avec comme exemple d’application :

protected void clickElementTableau(int ligne, int colonne) {
    Selecteur cellule = new Selecteur("la cellule située sur la ligne " + ligne + " et la colonne " + colonne + " du tableau", By.xpath("//tr[" + ligne + "]/td[" + colonne + "]"));
    logger.info("Clic sur " + cellule.getNom());
    driver.findElement(cellule.getChemin()).click();
}

En sortie, vous aurez des logs en langage naturel, par exemple : « Clic sur la cellule située sur la ligne 3 et la colonne 4 du tableau ».

Rappels sur les xPath

  • les xPath sont en base 1, c’est-à-dire que le premier élément d’une série a comme index 1, contrairement, par exemple, aux tableaux et aux listes Java dont le premier élément a pour index 0.
  • les xPath sont sensibles à la casse quand on fait une recherche sur du texte ou un attribut

Solutions

Il existe souvent plusieurs solutions possibles, celles ci-dessous ne sont que des possibilités.

1. Un xPath pour récupérer la deuxième cellule qui contient « Pomme » :

(//td[contains(., 'Pomme')])[2]

2. Un xPath pour récupérer la première case des lignes dont la colonne 3 contient la chaîne 700 :

//td[3][contains(., '700')]//../td[1]

3. Un xPath pour récupérer la deuxième case après la cellule qui contient « Pomme-cannelle » :

//td[contains(., 'Pomme-cannelle')]/following-sibling::td[2]

4. Un xPath pour récupérer les cases qui contiennent des prix entre 300 (inclus) et 630 (exclu) :

//td[. >= 300 and 630 > .]

5. Un xPath pour récupérer les cases qui ne contiennent pas de « e » minuscule (cases de l’entête du tableau comprises) :

//td[not(contains(., 'e'))] | //th[not(contains(., 'e'))]

6. Un xPath pour récupérer les lignes qui contiennent une case dont le texte commence par « Oups » :

//td[starts-with(., 'Oups')]/parent::tr

7. Un xPath pour récupérer les cellules du tableau (hors entête) qui contiennent la chaîne « de », qu’elle soit en minuscules ou en majuscules :

//td[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'de')]

Conseil : si possible encapsuler cette transformation de casse dans une fonction.

8. Un xPath pour récupérer les noms des fruits dont la description contient le texte « est » et dont le prix est entre 300 (inclus) et 630 (exclu).

//td[. >= 300 and 630 > .]/preceding-sibling::td[contains(., 'est')]/preceding-sibling::td

Même exercice pour les sélecteurs CSS

Mise à jour du 24 juin 2019 : nous avons découvert, grâce à une personne sur le Slack de la Test Automation University, un exercice en ligne pour s’entraîner à écrire des sélecteurs CSS. Un petit jeu interactif réjouissant et très bien fait : découvrez CSS Diner !

Tests autos d’applis lourdes : le défi du gratuit

Attention, cet article date d’il y a plusieurs années et ne correspond peut-être plus à l’état de l’art. Pour découvrir nos derniers articles, nous vous invitons à consulter la page d’accueil de notre blog !

________________

Vous venez du monde de Selenium, vous aimez ce framework gratuit, sa souplesse, la beauté du pattern page-object et son DSL aux petits oignons. Magnifique, mais aujourd’hui vous devez automatiser les tests d’une application lourde.

Il y a quelques années encore, choisir un outil d’automatisation gratuit et open-source pour ce genre de tests était un sérieux défi et présentait de nombreux risques : frameworks peu maintenus, résultats instables… Une solution propriétaire telle qu’UFT pouvait alors s’imposer comme un choix par défaut.

Est-ce différent aujourd’hui ? Nous pensons que certaines solutions sont désormais assez mûres pour être choisies au sein de projets de test ambitieux.

Pywinauto : go !

Ce nom incongru est la contraction de Python, Windows et Automation. Ce framework de test a vu le jour en 2006 (!), et donne lieu à des releases régulières depuis 2015. La version 1.0 n’est pas encore sortie (modestie de la part de Vasily Ryabov, développeur de Pywinauto ?) mais à l’utilisation, l’outil est parfaitement stable.

Avantages de Pywinauto

Utilisable dans tout IDE Python, la solution permet à l’utilisateur d’architecturer les projets de tests comme bon lui semble. Nous avons opté pour le pattern page-object, un grand classique qui permet de maintenir facilement les tests.

Principes

Identification des objets pour Pywinauto

L’identification des objets d’application Windows peut se faire grâce à des outils comme Inspect.exe, fourni avec le SDK Windows. UI Spy peut aussi être envisagé, mais il n’est plus maintenu par Microsoft. Cependant, à l’usage, nous l’avons trouvé plus rapide.

Grâce à ces utilitaires, il est possible de connaître les paramètres des objets, et donc de les identifier pour interagir avec eux (voir la liste des paramètres gérés par Pywinauto).

Interaction avec les objets avec Pywinauto

Dans l’exemple ci-dessous, nous déclarons ainsi une page ainsi que quelques objets, en utilisant au besoin plusieurs paramètres.

class AccueilPage:

    def __init__(self, test):
        self.ECRAN_ACCUEIL = test.app.window(title='Ma super appli - Accueil')
        self.INPUT_NOM = self.ECRAN_ACCUEIL.child_window(control_type="Edit", auto_id='23', found_index=0)
        self.INPUT_PRENOM = self.ECRAN_ACCUEIL.child_window(control_type="Edit", auto_id='24', found_index=0)

Dans l’architecture que nous avons mise en place, un test doit être invoqué au moment de l’appel du constructeur de la page. Ce test est initialisé au début de chaque scénario et correspond à la classe suivante :

class Test:
    app = Application(backend='uia')

    def __init__(self):
        parser = argparse.ArgumentParser()
        parser.add_argument("--log", help="enable logging", type=str, required=False)
        args = parser.parse_args()

        actionlogger.enable()
        logger = logging.getLogger('pywinauto')
        if args.log:
            logger.handlers[0] = logging.FileHandler(args.log)

        self.app = Application(backend='uia').start(r'C:cheminversmasuperappli.EXE')

Selon la façon dont l’application a été développée, certains objets ont un titre qui est affiché dans l’arborescence, ici d’UI Spy :

Dans cet exemple, on peut interagir avec le bouton « Appliquer » en utilisant le mot-clé title :

test.app.window(title='Appliquer')

Points d’attention

Certains objets complexes, par exemple des cellules de tableaux spécifiques, n’apparaissent pas toujours dans l’arborescence. Ce n’est pas nécessairement un problème provenant de Pywinauto, mais il faut garder en tête des solutions de contournement, par exemple la reconnaissance d’image que l’on trouve entre autres avec Sikulix.

Verdict

De notre côté, c’est un go ! Pywinauto a toute sa place au sein d’un POC, et a en outre l’avantage d’avoir un support efficace. Vasily Ryabov est très réactif sur Github aussi bien que sur StackOverflow, ce qui permet d’aller au-delà des blocages éventuels. EDIT du 19/20/2020 : et c’est toujours vrai en 2020 !

Winium : no go !

Framework basé sur Selenium et adapté aux applications Windows, la promesse de Winium est (était) séduisante… Malheureusement, il n’est plus maintenu et certains bugs empêchent de s’en servir efficacement. Nous vous expliquons cependant ce qui nous a plu et déplu dans cet outil, qui reste une excellente initiative.

Avantages de Winium

Pour un habitué de Selenium, la transition se fait en douceur ; les mêmes méthodes peuvent être utilisées dans la plupart des cas.

Vous ne rêvez pas ! On dirait bien un test Selenium…

Problèmes rencontrés

Identification des éléments

Afin d’interagir avec les objets de l’application à tester, plusieurs méthodes sont possibles (mais moins que pour Pywinauto). De la plus à la moins robuste, on peut procéder :

  • Par Id
  • Par Name
  • Par ClassName
  • Par Xpath

Les trois premiers paramètres, que l’on peut trouver avec les outils mentionnés précédemment, sont les plus pratiques à utiliser. Malheureusement, ils ne sont pas toujours renseignés. Il faut  alors avoir recours à la recherche par xPath, ce qui peut être plus difficile dans ce contexte que dans celui d’un DOM HTML.

Quelle que soit la méthode utilisée, les éléments semblent être trouvés d’autant plus lentement que l’interface compte un grand nombre d’objets.

Comme solution de contournement pour accélérer le test, nous avons utilisé Sikulix, avec toutes les questions que cela soulève côté maintenance.

Bug de focus

Nous avons relevé un bug assez ennuyeux : il arrive que l’application cible ne reçoive pas le focus pendant les tests. Pour peu que la fenêtre de l’IDE soit affichée, on se retrouve à taper l’identifiant en lieu et place du script de test.

A part fermer les fenêtres inutiles et réduire la fenêtre de l’IDE après le lancement de la campagne, nous n’avons pas trouvé de solution à ce problème.

Des nouvelles du créateur de Winium

Les logiciels ne poussent pas dans les arbres, en l’occurrence Winium est la création de Nick Abalov, actuellement développeur chez Badoo. Sur Twitter, il a gentiment répondu à notre question « Winium est-il mort ? » :

For Windows Desktop automation you might want to look into https://t.co/2agvjg2Kdj

— Nick Abalov (@NickAb) 20 décembre 2018

<script>

A suivre alors, WinAppDriver présentant cependant la contrainte de nécessiter un environnement Windows 10.

Verdict

Pour nous, c’est donc un no-go. Il est possible de mettre en place de petits projets d’automatisation en utilisant Winium, mais entamer un projet ambitieux avec cet outil serait imprudent.

Et vous, quels frameworks gratuits d’automatisation d’applications lourdes utilisez-vous ?

Gérer les paramètres Jenkins quand on est maniaque (ou flemmard)

Problème : des paramètres Jenkins interdépendants

Il n’est pas rare, quand on construit un build Jenkins avec des paramètres, que ceux-ci dépendent les uns des autres.

Exemple de configuration

Vous avez un paramètre de choix « Type de plat » avec comme valeurs possibles « Entrée », « Plat », « Dessert » et « Après-dessert » (un après-dessert est toujours le bienvenu).

Vous avez un autre paramètre de choix « Plat » avec comme valeurs possibles « Champignons à la grecque », « Spaghettis au roquefort », « Brownie » et « Salade de kiwis ».

Vous avez en tête un certain nombre de règles.

  • Les champignons à la grecque peuvent être commandés en tant qu’entrée ou plat principal
  • Les spaghettis au roquefort sont un plat
  • Il est possible de commander un brownie en dessert, mais pas en après-dessert, car ce ne serait pas raisonnable
  • La salade de kiwis peut être commandée en dessert ou en après-dessert.

Pourquoi cette configuration pose-t-elle problème ?

Possibilité 1 : vous êtes prudent (ou maniaque) et aimeriez interdire certaines combinaisons incohérentes.

Possibilité 2 : vous êtes flemmard et aimeriez gagner un peu de temps au moment de lancer un build.

Heureusement que la vie est bien faite et qu’il existe un plugin rien que pour vous.

Solution : Active Choices, un plugin Jenkins pour créer des conditions entre paramètres

Autrefois appelé Uno-Choice, le plugin Active Choice est gratuit et disponible directement depuis le gestionnaire de plugins de Jenkins.

Exemple d’utilisation d’Active Choices

Pour le cas énoncé, nous allons ajouter au job un paramètre de choix. Veillons simplement à ce que son nom soit en un seul mot, en écrivant « Type de plat » en camelcase. C’est important pour pouvoir ensuite utiliser ce nom comme une variable.

Maintenant, au lieu de créer un autre paramètre de choix classique, ajoutons un paramètre de type « Active Choices Reactive Parameter ».

Il faut maintenant cocher la case « Groovy Script », qui va nous permettre d’écrire nos règles. Ici, le script correspondant est le suivant :

if (TypeDePlat.equals("Entrée")) {
    return ["Champignons à la grecque"]
} else if (TypeDePlat.equals("Plat de résistance")) {
    return ["Champignons à la grecque", "Spaghettis au roquefort"]
} else if (TypeDePlat.equals("Dessert")) {
    return ["Brownie", "Salade de kiwis"]
} else if (TypeDePlat.equals("Après-dessert")) {
    return ["Salade de kiwis"]
} else {
    return ["Des petits cailloux ramassés dans l'allée"]
}

On indique une valeur par défaut dans le Fallback Script (par prudence, veiller à ce qu’elle soit cohérente avec la première valeur du paramètre de choix standard) :

return ["Champignons à la grecque"]

Maintenant, il faut simplement préciser à quel paramètre on se réfère.

Après sauvegarde de la configuration, lorsqu’on lance un build avec des paramètres, la liste déroulante des plats se met à jour en fonction de la valeur choisie dans la liste des types de plats. Pratique !

Ordre d’apparition des paramètres

Si vous êtes vraiment maniaque, vous remarquerez que les paramètres s’affichent dans la liste déroulante dans le même ordre que celui du script, et que par défaut, c’est le premier élément de la liste qui sera choisi.

Pour modifier cette valeur sans changer l’ordre alphabétique, au lieu de mettre l’élément voulu au début de la liste, il suffit de lui apposer « :selected« , comme ci-après :

return ["Champignons à la grecque", "Spaghettis au roquefort:selected"]

Conclusion

Le plugin offre de nombreux paramétrages possibles ; prenez le temps de les essayer, ça en vaut la peine !

Vous aimerez peut-être…

SonarQube met son grain de sel dans Gitlab !

Cas d’usage : JSoup et Gatling pour repérer les liens morts

L’automatisation des tests, c’est votre dada ? Voilà une ressource qui en parle en long, en large et en travers !

Logs Selenium : passez au niveau big boss

Des logs Selenium lisibles pour tous

Nous parlions de ce syndrome dans un précédent article ; le testeur chargé de l’automatisation des tests est parfois le seul à comprendre ses logs (on a appelé ça la logopathie). Cela réduit l’utilité des automates qui se retrouvent auréolés d’un mystère inutile.

Pour poursuivre dans cette série de bonnes pratiques d’automatisation des tests, nous vous proposons aujourd’hui plusieurs approches pour améliorer vos logs Selenium, sans le moindre framework hipster, juste avec du bon vieux Java. Nous utiliserons un système de logging des plus classiques, à savoir Log4J2, réglé sur la verbosité « INFO ». Libre à vous ensuite d’exploiter ces logs dans l’outil de reporting de votre choix (Allure par exemple). Allez, c’est parti !

Le script Selenium niveau débutant : rien que les logs d’erreur par défaut

Exemple de script Selenium

Voici un script très optimiste qui vérifie sur un célèbre site de vente calédonien que la première Peugeot 107 à vendre coûte 10 000 francs pacifiques ou moins.

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import java.util.concurrent.TimeUnit;

public class TestLogs {

    WebDriver driver;

    @Before
    public void setUp() {
        System.setProperty("webdriver.chrome.driver", "webdrivers/chromedriver.exe");
        driver = new ChromeDriver();
        driver.manage().timeouts().implicitlyWait(15, TimeUnit.SECONDS);
    }

    @Test
    public void testLogs(){
    driver.get("https://voitures.nc/");
        WebElement element = driver.findElement(By.id("recherche"));
        element.sendKeys(Keys.DELETE);
        element.sendKeys("107");
        driver.findElement(By.cssSelector("input[value='OK']")).click();
        driver.findElement(By.xpath("//a[contains(., 'Offre')]")).click();
        driver.findElement(By.xpath("(//table[contains(@href, 'detail_annonce')]//img)[1]")).click();
        Assert.assertTrue("ERREUR : le prix affiché est supérieur à " + 10000, Integer.parseInt(driver.findElement(By.xpath("//div[contains(@id, 'detail')]")).getText(). split("Prix : ")[1].split(" F cfp")[0]. replaceAll("\s+", "")) < 10000);
    }

    @After
    public void tearDown(){
        driver.quit();
    }
}

Problèmes du script Selenium

Comment on va maintenir ça ?

Ce qui saute déjà aux yeux avec ce script, c’est qu’il enfreint la règle de séparation du test et des objets pages. Si le Page Object Model ne vous éveille aucun souvenir, allez jeter un œil à cet article.

Ecrivez tous vos tests de cette façon et vous obtiendrez un magnifique plat de spaghettis impossible à maintenir !

Mais ce qui nous dérange le plus ici, c’est qu’en cas d’échec, on n’aura pas beaucoup d’informations

Exemples de logs cryptiques

Si la première Peugeot 107 affichée coûte plus de 10 000 XPF, on se retrouvera avec ces logs :

java.lang.AssertionError: ERREUR : le prix affiché est supérieur à 10000

at org.junit.Assert.fail(Assert.java:88)
 at org.junit.Assert.assertTrue(Assert.java:41)
 at TestLogs.testLogs(TestLogs.java:33)
 // [...] Et encore plein de lignes qu'on vous épargnera

Et rien avant ce problème d’AssertionError. Là, on a bien le test en tête, on comprend ce qui s’est passé. Cela dit, dans 6 mois, on ne saura plus ce qui était censé coûter 10 000 XPF maximum, sur quel site, etc…

Et si jamais le bouton de recherche n’est pas trouvé, les logs remontent cela, ce qui est moins parlant :

*** Element info: {Using=id, value=recherche}

at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
 at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
// [...]

Heureusement que le champ de recherche a un id et que celui-ci est explicite, mais ce n’est pas toujours le cas… Par exemple, sur le site d’Amazon, le champ de recherche a pour id « twotabsearchtextbox », ce qui est un peu moins compréhensible.

Le script Selenium niveau supérieur : chaque étape est logguée

Dans le bloc ci-dessous, le test a bien changé, il répond maintenant au pattern du Page Object Model. Il est beaucoup plus court, et surtout il est plus lisible grâce à des noms de méthodes explicites. Son setup et son teardown sont remontés d’un cran et se trouvent dans une nouvelle classe, « AbstractTest », dont hérite le test ; de cette façon, ils pourront être utilisés dans tous les tests.

La page invoquée, VoituresNcPage, a également son propre objet, qui contient ses propres méthodes.

Et les méthodes de bas niveau utilisées dans l’objet VoituresNcPage sont elles-mêmes remontées dans une classe mère, AbstractPage.

De cette façon, on respecte les célébrissimes principes DRY (don’t repeat yourself) et KISS (keep it super simple).

Le nouveau test Selenium

import org.junit.Test;
import pages.VoituresNcPage;
import utils.AbstractTest;

public class TestLogs extends AbstractTest {

    @Test
    public void testLogs() {
        accesURL("https://voitures.nc/");
        VoituresNcPage voituresNCpage = new VoituresNcPage(this);
        voituresNCpage.rechercherProduit("107");
        voituresNCpage.accesAuxOffres();
        voituresNCpage.classerParPrix();
        voituresNCpage.deplierAnnonceNumero(1);
        voituresNCpage.verifierPrixInferieurOuEgalA(10000);
    }

}

Les méthodes de haut et de bas niveau

Toutes les méthodes de bas niveau donnent lieu à une ligne de log. Les méthodes de vérification donnent lieu à un log en cas d’échec. Un exemple avec la méthode qui permet de classer les produits par prix :

// Dans VoituresNcPage. On trouve les objets et les méthodes spécifiques à la page.

private final By BTN_CLASSER_PAR_PRIX = By.xpath("(//a[contains(@href, 'orderBy=prix')])[1]");
public void classerParPrix(){
   clickElement(BTN_CLASSER_PAR_PRIX);
}

[...]
// Dans AbstractPage. On trouve des méthodes qui pourraient servir à toutes les pages web du monde. Des vérifications sont faites avant chaque interaction, pour vérifier que l'élément concerné est bien présent, et logger ce problème le cas échéant.

protected int timeOut = 5; // Cette valeur sert à configurer combien de temps un élément doit être "attendu" avant qu'on ne considère qu'il est absent de la page.

protected void clickElement(By by) {
 logger.info("Clic sur l'élément '" + getPathFromBy(by) + "'"); // Un premier log de bas niveau
 assertElementPresent(by);
 driver.findElement(by).click();
}

protected String getPathFromBy(By by){
    return by.toString().split(": ")[1]; // On a choisi de faire ce split pour gagner en concision, mais vous aurez peut-être envie de conserver le type de sélecteur.
}

public void assertElementPresent(By by) {
    if(!isElementPresent(by)){
        logger.error("ERREUR : '" + getPathFromBy(by) + "' n'a pas été trouvé(e) sur la page."); // Un deuxième log de bas niveau
    }
    assertTrue(isElementPresent(by));
}

public boolean isElementPresent(By by) {
    boolean isElementPresent = false;
    int time = 0;
    while (!isElementPresent && time < timeOut) {
        if (isElementPresentNow(by)) {
            isElementPresent = true;
        } else {
            isElementPresent = false;
        }
        sleep(1);
        time++;
    }
    return isElementPresent;
}

// Un petit "hack" qui permet d'ignorer temporairement le timeout.
public boolean isElementPresentNow(By by) {
    driver.manage().timeouts().implicitlyWait(0, TimeUnit.MILLISECONDS);
    boolean isElementPresent = (driver.findElements(by).size() != 0);
    driver.manage().timeouts().implicitlyWait(timeOut, TimeUnit.MILLISECONDS);
    return isElementPresent;
}

Les nouveaux logs

Désormais, les logs ressemblent maintenant à ça :

Accès à l'URL https://voitures.nc/
Renseignement du texte '107' dans l'élément 'recherche'
Clic sur l'élément 'input[value='OK']'
Clic sur l'élément '//a[contains(., 'Offre')]'
Clic sur l'élément '(//a[contains(@href, 'orderBy=prix')])[1]'
Clic sur l'élément '(//table[contains(@href, 'detail_annonce')]//img)[1]'
ERREUR : le prix affiché est supérieur à 10000

On comprend un peu mieux ce qui s’est passé. Un testeur n’ayant pas lui-même écrit ce test peut, en se concentrant un peu, retracer le scénario. Ca peut suffire, mais une fois encore, si on veut partager nos logs, on se retrouve dépendants de la clarté des sélecteurs.

Le script Selenium niveau big boss : des logs compréhensibles par tout le monde

Jusqu’à maintenant, rien de bien original. Mais nous voulons maintenant aller un peu plus loin, en permettant à toute personne de comprendre parfaitement les logs, sans avoir besoin du moindre bagage HTML et CSS. On pense par exemple aux intervenants fonctionnels qui auraient besoin de lancer les tests automatisés en autonomie et d’analyser leurs résultats. Chaque étape sera détaillée en langage naturel, joliment et proprement, aussi bien que si on les avait décrites nous-mêmes. A ce stade, les logs du test seront aussi clairs que possible, et surtout à peu de frais. Ca vend du rêve ? Bien sûr que non, car c’est gratos ! C’est parti !

Ce que nous n’allons pas faire

Une solution serait d’étoffer chaque méthode de la classe VoituresNcPage par un log spécifique.

public void classerParPrix(){
   logger.info("Classement des produits par prix");
   clickElement(BTN_CLASSER_PAR_PRIX);
}

Pourquoi pas, si vous avez le temps d’écrire ces logs et que vous vous engagez à les maintenir. Mais on va partir du principe que vous voulez gagner du temps et faire au plus simple.

Notre proposition : bye bye le By

Pour identifier et interagir avec les éléments de la page (boutons, liens, champs…), Selenium propose une classe dédiée : By. Elle permet de les sélectionner par xpath, nom de classe CSS, sélecteur CSS, id, texte de lien hypertexte (ou partie de ce texte), name, balise html…

private final By BTN_CLASSER_PAR_PRIX = By.xpath("(//a[contains(@href, 'orderBy=prix')])[1]");

Mais nous allons laisser tomber ce bon vieux By, ou plutôt l’améliorer pour la beauté de vos logs. Nous allons passer par une nouvelle classe, que nous allons appeler « Selecteur ».

// Dans AbstractPage

public class Selecteur {
   public String nom; // C'est cette variable qui va contenir la description de chaque élément en langage naturel !
   public By chemin;

   public Selecteur(String nom, By chemin){
      this.nom = nom;
      this.chemin = chemin;
   }

   public String getNom(){
      return nom;
   }

   public By getChemin(){
      return chemin;
   }
}

Remplacement des By par des Selecteurs

Dans les méthodes

Maintenant, on va modifier nos méthodes de bas niveau qui utilisent la classe By, en la remplaçant par la classe Selecteur. Par exemple :

// Dans AbstractPage

protected void clickElement(Selecteur selecteur) {
    logger.info("Clic sur " + selecteur.getNom());
    assertElementPresent(selecteur);
    driver.findElement(selecteur.getChemin()).click();
}
protected String getPathFromBy(Selecteur selecteur){
    return selecteur.getChemin().toString().split(": ")[1];
}
public void assertElementPresent(Selecteur selecteur) {
    if(!isElementPresent(selecteur.getChemin())){
        logger.error("ERREUR : " + selecteur.getNom() +
                " n'a pas été trouvé(e) sur la page. " +
                " (sélecteur : '" + getPathFromBy(selecteur) + "')");
    }
    assertTrue(isElementPresent(selecteur.getChemin()));
}
[...]

Dans les objets Page

A cette étape, il reste maintenant à transformer les sélecteurs « By » qui se trouvent dans vos objets pages. Et en premier paramètre, à vous de jouer : vous devez décrire, du mieux que vous pouvez, ce qu’est l’objet d’un point de vue fonctionnel ; à quoi il sert. Vous devez le faire concisément, mais surtout précisément, pour éviter toute ambiguïté.

private final Selecteur BTN_CLASSER_PAR_PRIX = new Selecteur("le bouton permettant de classer les articles par prix", By.xpath("(//a[contains(@href, 'orderBy=prix')])[1]"));

Résultats du refacto

Une fois que c’est fait, on reteste pour voir à quoi ressemblent maintenant les logs !

Accès à l'URL https://voitures.nc/
Renseignement du texte '107' dans le champ de recherche
Clic sur le bouton permettant de valider la recherche
Clic sur le lien vers les offres
Clic sur le bouton permettant de classer les articles par prix
Clic sur le bouton permettant de déplier l'annonce n°1
ERREUR : le prix affiché est supérieur à 10000

Là, on a toutes les informations et on comprend tout. Et que se passe-t-il si on regarde, comme au début de l’article, ce que montrent les logs si jamais le champ de recherche a disparu ?

Accès à l'URL https://voitures.nc/
Renseignement du texte '107' dans le champ de recherche
ERREUR : le champ de recherche n'a pas été trouvé(e) sur la page. (sélecteur : 'recherche')

Bien, on comprend aussi.

[BONUS] Le script Selenium niveau serial-bugkiller : le générateur de rapport de bug

Tels quels, vos logs expliqueront ce qu’on fait les automates de test. En changeant peu de choses à la proposition précédente, vos logs détailleront les étapes à suivre pour reproduire leur comportement. Et du coup, vous mettrez un peu moins de temps à rédiger vos rapports de bugs, car votre partie « Etapes pour reproduire » sera déjà écrite par votre automate, et n’importe qui pourra la comprendre, quel que soit son niveau de connaissance du projet.

Rendez-vous sur l'URL https://voitures.nc/
Renseigner le texte '107' dans le champ de recherche
Cliquer sur le bouton permettant de valider la recherche
Cliquer sur le lien vers les offres
Cliquer sur le bouton permettant de classer les articles par prix
Cliquer sur le bouton permettant de déplier l'annonce n°1
ERREUR : le prix affiché est supérieur à 10000

Vous n’aurez peut-être pas besoin de ce type de logs, ceux de l’étape précédente vous suffiront peut-être, mais cela vous donnera peut-être d’autres idées d’utilisations des logs.

Conclusion

Pour conclure, nous pourrions dire qu’il est illusoire de rechercher une Peugeot 107 à 10 000 XPF ou moins.

Mais aussi, que vos logs sont un atout dont vous pouvez tirer une grande valeur, et à peu de frais. Ce que nous avons exploré dans cet article n’est qu’un exemple parmi d’autres de valorisation des logs.

Et vous, comment utilisez-vous vos logs de tests automatisés ? Comment les avez-vous améliorés au fil du temps ?

Crédit image

L’image de couverture est une reprise de Miss Auras (Le Livre rouge), Sir John Lavery (1856-1941).

SonarQube met son grain de sel dans Gitlab !

Attention, cet article est ancien ! Certaines parties sont peut-être inadaptées à votre stack actuelle. Pour découvrir nos nouveaux article sur le test et la qualité logicielle, nous vous conseillons de consulter la page d’accueil de notre blog. Amusez-vous bien !

La galaxie SonarQube

Dans de précédents articles, nous avons présenté SonarQube, son interface ainsi que son intégration avec Jenkins, et SonarLint, son acolyte qui permet aux développeurs de vérifier la qualité de leur code avant de le commiter. Aujourd’hui, nous voulons affiner encore les choses, et différencier l’analyse des branches features et master.

Cet article présente un applicatif open-source, disponible en l’état et sans garantie. Il est de la responsabilité du SI de vérifier qu’il est conforme à sa politique de sécurité d’une part, et de respecter sa licence d’autre part. Merci à Gabriel Allaigre, de Talanlabs, pour le plugin Sonar-Gitlab !

La procédure suivante met en oeuvre SonarQube, Gitlab et Jenkins en environnement Linux.

Problème initial : trop ou trop peu de données qualimétriques ?

Entre surcharge informationnelle…

Dans un projet donné, il arrive que chaque branche (de feature ou même de bug) fasse l’objet d’une analyse SonarQube à chaque fois que du nouveau code est poussé dessus. Les résultats de cette analyse sont alors stockés dans la base de données et affichés dans l’interface.

Le problème, c’est qu’il en résulte rapidement une liste interminable de « projets » SonarQube qui n’en sont pas ; ce ne sont que des branches ! Ces résultats sont conservés indéfiniment alors qu’ils perdent leur intérêt dès que les branches sont fusionnées.

Comment éviter ce bruit tout en conservant le feedback de SonarQube ?

… et isolement des informations

Par ailleurs, il est assez courant d’entendre des témoignages de ce type : « Nous avons installé SonarQube et des analyses sont régulièrement lancées sur nos projets, mais en pratique on ne les regarde pas souvent. » Effectivement : SonarQube est simple à installer, simple à configurer et simple à utiliser. Le plus difficile est de l’intégrer pleinement aux modes de travail des équipes.

Le plugin présenté ici vise à résoudre le premier problème, tout en apportant un élément de solution au second.

Comprendre les lancements d’analyse SonarQube

Il faut d’abord savoir que SonarQube dispose de 3 modes de lancement :

  • Analysis : il s’agit du mode de lancement par défaut de SonarQube. Tous les fichiers sont analysés et SonarQube crée ou met à jour un projet dans sa base de données.
  • Preview : tous les fichiers sont également analysés mais les résultats ne sont pas stockée dans la base de données.
  • Incremental : seul le fichier courant ou les derniers fichiers modifiés sont analysés. Il s’agit du mode de lancement par défaut de SonarLint.
    Source : https://blog.sonarsource.com/analysis-vs-preview-vs-incremental-preview-in-sonarqube/

Ce sont sur ces modes de lancement que nous allons nous appuyer pour différencier les analyses des branches features et master :

  • Pour la branche master, nous allons continuer d’utiliser le mode analysis, et stocker les résultats dans la base de données.
  • Pour les branches temporaires, nous allons basculer sur le mode preview, ce qui va nous permettre de générer le rapport et de le visionner… sur Gitlab !

L’idée est de permettre à un utilisateur Gitlab dédié (que l’on nommera tout simplement SonarQube) de commenter les commits en indiquant les mauvaises pratiques détectées dans les portions de nouveau code.

Mise en pratique

Récupération du plugin

La dernière version stable du plugin Sonar-Gitlab est disponible depuis le centre de mise à jour de SonarQube, vous pouvez l’y installer directement.

Si vous souhaitez utiliser la toute dernière version du plugin, vous devrez récupérer le projet Sonar Gitlab Plugin sur la page Github du projet, placer le jar dans le répertoire « plugins » de Sonarqube (SONARQUBE_HOME/extensions/plugins) puis redémarrer SonarQube.

Configuration de Gitlab

  1. Créer un compte Gitlab avec comme nom SonarQube
  2. Se connecter sur Gitlab en tant qu’administrateur et attribuer à l’utilisateur SonarQube le profil « Développeur » dans les groupes contenant les projets à analyser
  3. Se connecter sur Gitlab avec l’utilisateur SonarQube et générer un token sur la page http://urldegitlab/profile/personal_access_tokens

Configuration de SonarQube

  1. Se connecter sur SonarQube en tant qu’administrateur
  2. Dans « Configuration », se rendre dans le tout nouvel onglet « Gitlab »
  3. Dans le formulaire qui s’ouvre, renseigner le token
  4. Remplir les champs « Global template » et « Inline template » en adaptant les templates proposés ici : https://gitlab.talanlabs.com/gabriel-allaigre/sonar-gitlab-plugin#examples

Configuration du projet à analyser

  1. Sur Gitlab, récupérer l’id du projet souhaité, qui se trouve dans les Settings du projet.
  2. Sur SonarQube, revenir à l’écran des projets, cliquer sur le projet concerné, puis sur « Configuration » > « Paramètres projet » > « Gitlab »
  3. Dans le formulaire qui s’affiche, renseigner l’id du projet récupéré dans Gitlab et, éventuellement, adapter les barrières qualité du projet
  4. Pour être analysé par SonarQube, un projet doit contenir un fichier sonar-project.properties. Ajouter les deux lignes suivantes à ce fichier :
     sonar.analysis.mode=${MODE_ANALYSE}
     ${GITLAB_CONFIG}

    A noter : les variables ${MODE_ANALYSE} et ${GITLAB_CONFIG} seront remplacées au moment du build Jenkins.

Configuration du job Jenkins

  1. Sur Jenkins, créer un build et ajuster les actions déclenchant le build ; ici, à chaque fois qu’un changement a lieu sur Gitlab
  2. Lors de l’étape de récupération des sources, prendre soin de ne cibler aucune branche en particulier.
  3. Ajouter une étape « Exécuter un script shell » avec le script suivant :
     ID_PROJET="124" # id du projet, à récupérer dans Gitlab
     URL_SONARQUBE="http://urldesonarqube"
     SHA_DERNIER_COMMIT=$GIT_COMMIT
     if ["$GIT_BRANCH" = "origin/master" ];
    then
     echo "Nous sommes sur la branche master. Les résultats de cette analyse seront enregistrés dans la base de données de SonarQube."
     sed -i "s/${MODE_ANALYSE}/publish/g" sonar-project.properties # on suppose que ce fichier est à la racine du projet. Adapter le chemin si besoin.
     sed -i "s/${GITLAB_CONFIG}//g" sonar-project.properties
     else
     echo "Nous sommes sur une autre branche que master. Les résultats de cette analyse seront enregistrés dans les commentaires de l'utilisateur SonarQube dans Gitlab."
     sed -i "s/${MODE_ANALYSE}/preview/g" sonar-project.properties
     # Configuration Gitlab
     sed -i "s/${GITLAB_CONFIG}/sonar.gitlab.commit_sha=${CI_BUILD_REF}nsonar.gitlab.ref_name=${CI_BUILD_REF_NAME}nsonar.gitlab.project_id=${ID_PROJET}nsonar.host.url=${URL_SONARQUBE}nsonar.issuesReport.console.enable=true/g" sonar-project.properties
     sed -i "s/${CI_BUILD_REF}/$SHA_DERNIER_COMMIT/g" sonar-project.properties
     sed -i "s/${CI_BUILD_REF_NAME}/$SHA_DERNIER_COMMIT/g" sonar-project.properties
     fi
  4. Ajouter l’étape de lancement de l’analyse SonarQube.

Intégrer l’analyse de code à tous les niveaux

Récapitulons. Désormais, à chaque changement dans le dépôt Gitlab, un build Jenkins sera lancé. Si c’est la branche master qui est concernée, un rapport sera enregistré dans la base de données de SonarQube ; sinon, des commentaires seront postés sur le commit pour indiquer d’éventuelles mauvaises pratiques dans le nouveau code introduit.

Bonnes analyses !

Vous aimerez peut-être…

Sondez votre code avec SonarQube

La qualité de code en direct avec SonarLint !

5 graphiques Jira pour mieux communiquer sur les anomalies

Relecture 2023 : bien que cet article date un peu, nous confirmons qu’il est toujours d’actualité 😉 Bonne lecture et amusez-vous bien à créer tous les graphiques qui vous aideront dans votre quotidien de QA !

Avoir les bonnes métriques au bon moment

« Combien a-t-on d’anomalies ouvertes ? »

Voilà une question que les QA entendent parfois, mais qui n’amène qu’une réponse assez sommaire.

Le produit comptabilise 200 anomalies ouvertes ? Simplement en tenant compte de la criticité des bugs, on découvrira peut-être qu’il est de meilleure qualité qu’il y a quelques mois, où il n’en comptait que 150.

Une liste d’anomalies en dit long sur son projet. En prenant le temps de l’analyser, on peut être en mesure de répondre aux questions suivantes :

  • Où se regroupent les défauts ?
  • Comment la qualité du produit évolue-t-elle dans le temps ?
  • Le nombre de régressions augmente-t-il ou diminue-t-il ?

Autant d’informations qui permettent de détecter aussi bien des risques projet que des risques produit.

Dans votre organisation, si la gestion des anomalies se fait sur Jira, cet outil vous sera d’une grande aide pour analyser et communiquer autour des anomalies de vos projets. Il permet de créer un certain nombre de graphiques, appelés « gadgets ». En voici quelques mises en pratique !

Dans cet article nous parlons indifféremment de « demandes » ou « tickets » Jira.

Prérequis : savoir comment gérer les filtres Jira

Les filtres constituent une fonctionnalité phare de Jira. Ils permettent aussi bien de retrouver rapidement un lot de demandes que de créer des graphiques à partir de celles-ci.

Après avoir effectué une recherche à plusieurs critères, il suffit d’enregistrer celle-ci en lui donnant un nom. Votre filtre est prêt à être utilisé. Pour cela, rendez-vous dans l’onglet « Tableaux de bord » et cliquez sur « Ajouter un gadget ».

Comptabiliser une dimension des anomalies avec le graphique « Statistiques de filtre bidimensionnel »

Le graphique « Statistiques de filtre bidimensionnel », très simple, permet de visualiser une facette donnée d’un lot de demandes. Ici, nous avons choisi en première dimension le champ dédié à la sévérité des bugs. Ce qui nuance, comme dit précédemment, le simple nombre de bugs.

Pour rappel, le lot d’anomalies est défini par le filtre choisi. Il peut être intéressant de comparer un graphique portant sur toutes les anomalies d’un projet, à un graphique portant uniquement sur les anomalies ouvertes de ce projet.

Sur ce graphique, on voit également le statut des différents bugs (la 2ème dimension), ce qui permet par exemple d’évaluer si l’effort de correction est bien dirigé vers les sévérités les plus élevées.

Identifier les zones à risque de l’application avec le graphique « Carte thermique »

Créer un nuage de bugs

L’un des 7 principes généraux du test énoncés par l’ISTQB met en valeur la tendance des bugs à se regrouper. Par expérience, les QA affectés depuis quelques temps à un projet donné sauront où focaliser l’effort de test, en particulier lors de ses tests exploratoires. Mais cela ne suffit pas : il faut aussi pouvoir mettre en évidence auprès de toutes les parties prenantes les zones sensibles de l’application, afin qu’elles y accordent une attention particulière.

Sur Jira, le gadget « Carte thermique » génère un nuage de mots qui affiche chacun d’eux avec une police proportionnelle à son nombre d’occurrences. Ces mots peuvent être issus d’une multitudes de critères. Dans un projet donné, chaque anomalie peut se voir attribuer une ou plusieurs étiquettes correspondant aux blocs fonctionnels concernés. La carte thermique portant sur les étiquettes permettra alors d’identifier les fonctionnalités les plus sensibles, comme ci-dessous.

Avantages de ce graphique

Cette vision peut aider à organiser la correction d’anomalies en mettant entre parenthèses le seul critère de priorité. Cela présente plusieurs avantages. Quand une personne se charge d’un lot d’anomalies concentrées sur une même fonctionnalité :

  • Elle termine généralement le travail plus rapidement que si plusieurs personnes s’y étaient attelé les unes après les autres
  • Comme elle se plonge dans la fonctionnalité dans son ensemble, elle monte en compétence sur l’ensemble de ses aspects, ce qui pourra l’aider à la faire évoluer par la suite
  • A l’issue de cette correction, le client peut se sentir soulagé de constater que pour une fonctionnalité, tout fonctionne correctement
  • Des anomalies mineures ou triviales, qui auraient été ignorées sinon, peuvent être épongées.

Evaluer la dette technique avec le graphique « Demandes créées comparées aux demandes résolues »

Comme dit précédemment, le nombre d’anomalies ouvertes peut s’interpréter de différentes façons. Il est aussi intéressant d’analyser le graphique comparant le nombre de demandes créées au nombre de demandes résolues, pour adapter si besoin l’effort de correction d’anomalies. Un gadget dédié est disponible sur Jira.

Très parlant, il permet de voir en un coup d’oeil si la dette technique est en train de se résorber ou au contraire de s’accentuer.

Identifier les moments critiques avec le graphique « Plage de demandes »

« Cette appli est de plus en plus buggée ! »

Le graphique « Plage de demandes », ou « Plage de tickets », permet de communiquer sur le nombre d’ouvertures de bugs au fil du temps. Prenez le temps de bien expliciter les différentes hausses et baisses, qui peuvent être dues à des facteurs très différents (nouvel interfaçage avec un autre système complexe, congés d’une partie de l’équipe, livraison de nouvelles fonctionnalités, phase soutenue de correction d’anomalies, arrivée de nouvelles recrues dans l’équipe de test, refacto…)

Pensez à désactiver l’option « cumulative ». Sans cela, le graphique afficherait une courbe croissante, qui ne permettrait pas aussi bien d’identifier les pics de bugs.

Par exemple, sur le graphique ci-dessus, nous voyons qu’entre novembre et février l’équipe a ouvert énormément d’anomalies, puis que le nombre de rapports de bugs ouvert a baissé.

Evaluer le turn-over des bugs avec le graphique « Âge moyen »

Assez difficile à interpréter, ce graphique peut toutefois fournir des chiffres « choc » pour motiver la correction d’anomalies.

Un bémol cependant : ces chiffres sont de moins en moins parlants au fil du temps, car dans l’ensemble, il est normal que les bugs bloquants récents soient corrigés avant les bugs triviaux anciens.

Il peut être intéressant d’ajouter la notion de criticité à ce filtre.

Quand on considère le graphique ci-dessus, on peut supposer que le rythme de correction des anomalies a baissé et que de la dette technique s’est accumulée.

Partager les graphiques

Il est important que les parties prenantes puissent accéder aux graphiques à volonté. Cela permettra à tout un chacun de prendre quand il le souhaite la température du projet. Pour cela, plusieurs solutions :

  • Insérer ces graphiques dans un ou plusieurs tableaux de bord partagés (sur Jira)
  • Profiter de l’intégration Jira-Confluence pour intégrer les gadgets sur une page de wiki, ce qui permettra de commenter au fur et à mesure l’évolution des données affichées.
  • Périodiquement, envoyer des rapports (par mail ou autre) intégrant des copies d’écran de ces graphiques, avec un lien pour accéder à leur version dynamique.

Nous espérons vous avoir donné des idées, n’hésitez pas à partager vos propres graphiques avec nous !

Vous aimerez peut-être…

Testeur en contexte Agile, pour (re)découvrir le rôle des QA en contexte Scrum

L’épineuse question des KPI, parce que tous ces beaux graphiques Jira peuvent générer de précieuses métriques utiles à toute l’équipe et sa performance !

La qualité de code en direct avec SonarLint !

Attention, cet article est ancien et certains de ces éléments ne sont peut-être plus d’actualité. Pour découvrir nos articles les plus récents, nous vous invitons à découvrir la page d’accueil de notre blog !

A quoi sert SonarLint ?

Après notre présentation de SonarQube, nous vous proposons maintenant de prendre le problème de la qualité de code à la racine avec le linter SonarLint.

Qu’est-ce qu’un linter ?

Un linter est un utilitaire d’analyse de code. Ce type d’outil permet de déceler les éventuelles erreurs et problèmes syntaxiques. On parle de « linter son code » (en bon franglais !), ce qui consiste à lancer une analyse de code afin de le rendre plus maintenable, plus lisible et globalement plus qualitatif. Le linter ne procède pas aux corrections et modifications du code ; cela reste au développeur d’arbitrer sur la solution à mettre en œuvre.

Que fait SonarLint ?

SonarLint est un plugin permettant de réaliser les analyse de code SonarQube au fil des développements, directement dans l’IDE. Un bon coup de pouce pour identifier au plus tôt, c’est-à-dire avant même le commit, les points à corriger. De ce fait, le coût de la qualité du code est extrêmement réduit, voire invisible, à l’inverse des lourdes campagnes correctives qui interviennent sur du code déjà mergé depuis longtemps.

Ici, un exemple avec Intellij. Il est à noter que ce plugin est également disponible pour les IDE Eclipse et Visual Studio.

Installation du plugin SonarLint sur Intellij

  • Ouvrir Intellij
  • Aller dans « File » > « Settings » > « Plugins »
  • Cliquer sur « Browse repositories »
  • Rechercher « SonarLint », l’installer et redémarrer Intellij

Lancer une analyse SonarLint

Pour lancer une analyse, cliquez sur l’onglet « Analyze » d’Intellij. Vous verrez les options SonarLint dans une liste déroulante.

Si par exemple nous lançons une analyse sur tous les fichiers (attention, cette opération peut s’avérer assez longue sur les gros projets), nous obtenons une vue telle que celle-ci :

Un double-clic sur une anomalie permet d’accéder à la ligne de code en question, ainsi qu’à l’explication du problème afin de pouvoir le corriger en connaissance de cause.

Intégration de SonarLint avec un serveur SonarQube

Partagez vos règles SonarQube sur tous les postes

Très bien, nous pouvons maintenant voir très rapidement les défauts de notre code dans la console SonarLint. Mais ce qui serait encore mieux, ce serait de partager les mêmes règles que le reste de l’équipe, à savoir les règles définies dans le serveur SonarQube central. Ces règles correspondent au(x) profil(s) qualité utilisé(s) pour le projet.

Pour ce faire :

  • Aller dans « File » > « Settings » > « Other settings » > « SonarLint General Settings ».
  • Ajouter un serveur SonarQube. Pour ce faire, il y a besoin d’un token (souvenez-vous, nous en avions besoin aussi pour interfacer SonarQube avec Jenkins !), ou d’un couple login/password.
  • Dans l’onglet « SonarLint Project Settings », associer le projet courant au projet correspondant dans SonarQube. Il est important que le nom du projet soit le même dans les deux outils.
  • De retour dans l’onglet « SonarLint General Settings », cliquer sur le bouton « Update binding ».

Désormais, vous partagez dans votre IDE le même profil qualité que votre projet SonarQube. Faites un test : introduisez un bug dans votre code, changez la priorité de ce bug dans SonarQube, cliquez de nouveau sur le bouton « Update binding » dans les Settings d’Intellij, et vous verrez que la priorité du bug changera également dans votre IDE lors de la prochaine analyse. Si ce n’est pas le cas, c’est que vous avez manqué une étape !

Résolution de version

Au moment de la mise à jour du lien SonarQube – SonarLint, une popup d’erreur peut survenir, avec un message comme celui-ci :

The following plugins do not meet the required minimum versions, please upgrade them: *** (installed: x.x, minimum: x.x).

Il s’agit la plupart du temps d’un package de règles d’un langage qui doit être mis à jour dans SonarQube. Pour régler ce problème de version :

  • Se rendre sur l’interface SonarQube
  • Se connecter avec un compte administrateur
  • Aller dans « Configuration » > « Système » > « Mises à jour »
  • Mettre à jour le module mentionné dans le message d’erreur.

Conclusion

Le plugin SonarLint représente une opportunité pour les développeurs, car il aide à renforcer la qualité de code au fur et à mesure de l’écriture. Moins de risque d’oublis donc, et un allègement significatif des retours produits lors de la revue de code.

Un point d’alerte toutefois : il faut veiller à ce que les règles définies dans le profil qualité soient pertinentes, c’est-à-dire adaptées à la fois au projet et à l’équipe concernée. Sans cela, une part plus ou moins importante des analyses produites par SonarLint sera tout simplement ignorée par les équipes de développement. Le syndrome de Cassandre dans le domaine de la qualité de code, cela vous parle ? Si oui, vous découvrirez avec plaisir un outil permettant d’en sortir

Vous aimerez peut-être…

Sondez votre code avec SonarQube

SonarQube met son grain de sel dans Gitlab !

Protractor + AngularJS : Structure d’un projet industrialisé

Note préalable : cet article date de 2017. Depuis, Protractor a atteint la fin de sa vie. Merci à lui et surtout aux personnes qui ont contribué à cet outil !

Diviser pour mieux tester

Bienvenue dans la 3ème et dernière partie de notre série d’articles sur la découverte des tests Protractor ! Dans l’article précédent, nous avons vu pourquoi et comment séparer les jeux de données et les scripts de test. Aujourd’hui, nous poursuivons sur notre lancée en isolant la description des pages et en structurant les dossier du projet.

  1. Installation et exemple de script
  2. Gestion des jeux de données et des dépendances du projet
  3. Structure d’un projet industrialisé

Gestion du référentiel d’objets avec le Page Object Model

Qu’est-ce que le Page Object Model ?

Le Page Object Model est un design pattern (ou patron de conception) qui consiste à regrouper en une même classe les identificateurs des composants web avec lesquels on souhaite intéragir, ainsi que les méthodes associées à ces composants. Le Page Object Model joue le même rôle dans les projets Selenium que l’Object Repository dans les projets UFT ou Katalon par exemple.

Dans les articles précédent, nous interagissons avec la page d’accueil du site d’Angular. Si nous nous servons d’un objet page pour la décrire, cela donne :

var SiteAngularPageAccueil = function() {
    this.champ_nom = element(by.model('yourName'));
    this.message_salutations = element(by.xpath("//div[contains(@class, 'well')]//h1"));

    this.entrerPrenom = function(prenom) {
        this.champ_nom.sendKeys(prenom);
    };
};

module.exports = SiteAngularPageAccueil;

La méthode entrerPrenom est vraiment simpliste ; en pratique, on pourrait s’en passer. Nous avons choisi de la créer simplement pour fournir un exemple d’utilisation de méthode d’objet.

Enregistrons ce code dans un fichier site-angular-page-accueil.js, que nous plaçons dans un dossier « pages ».

Utilisation de l’objet page dans le script

Mettons à présent à jour le cas de test en appelant l’objet page et en interagissant avec elle :

var donnees = require('./jeu-de-donnees-demo.js');
var using = require('jasmine-data-provider');
var SiteAngularPageAccueil = require('../pages/site-angular-page-accueil.js');

describe("Suite de test de la feature qui dit bonjour", function() {

    var pageAccueil = new SiteAngularPageAccueil();

    beforeEach(function () {
        browser.get(browser.baseUrl);
    });

    using(donnees.prenoms, function (data, description) {
        it("Chaîne de type " + description, function () {
            testMessageSalutations(data.prenom);
        });
    });

    using(donnees.faux_positif, function (data, description) {
        it("Si je rêvasse en écrivant mon script de test, j'obtiens un faux positif", function () {
            testMessageSalutations(data.prenomSaisi, data.prenomVoulu);
        });
    });

    function testMessageSalutations(prenomSaisi, prenomVoulu){
        pageAccueil.entrerPrenom(prenomSaisi);
        var prenomAVerifier = (prenomVoulu ? prenomVoulu : prenomSaisi);
        expect(pageAccueil.message_salutations.getText()).toEqual('Hello ' + prenomAVerifier + '!');
    }

});

Nous avons également créé une fonction testMessageSalutations pour éviter au maximum la duplication entre les deux cas de test.

Le script est maintenant concis et exempt de toute redite ! A ce stade, notre script de test a atteint un niveau de maintenabilité satisfaisant. Il peut jouer une multitude de cas de test différents, et n’est plus soumis aux changements de l’interface du site web.

Exemple de structure de projet Protractor

Pour augmenter la lisibilité de votre projet, nous vous suggérons de ranger dans des répertoires séparés vos fichiers de configuration, vos pages, vos suites de tests et vos jeux de données.

Le nombre de suites de test augmentant, n’hésitez pas à créer des sous-dossiers. Dans vos fichiers de configuration, il sera d’autant plus simple de sélectionner tel ou tel ensemble de suites de tests. Par exemple :

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['../specs/front-office/*.js'],
    baseUrl: 'https://www.monsupersiteweb.nc',
    framework: 'jasmine2'
};

Ici, toutes les suites de test se trouvant dans /spec/front-office seront jouées.

Vous avez maintenant tout ce qu’il vous faut pour démarrer votre projet de tests Protractor sur de bonnes bases. A vous de jouer maintenant ! Et vous souhaitez rester encore un peu avec nous, voici un petit bonus…

Bonus : le reporting Jasmine

Vous aurez beau écrire le plus beau code du monde, il y a peu de chances que celui-ci suffise à émouvoir aux larmes votre chef de projet, client ou patron. Un reporting clair et documenté est indispensable pour communiquer sur l’exécution des tests. C’est pourquoi nous ne voulions pas vous quitter avant d’évoquer la librairie Jasmine2 Screenshot Reporter.

Ajout de la librairie

Dans votre package.json, déclarez la dépendance voulue :

"dependencies": {
  // [...]
  "protractor-jasmine2-screenshot-reporter": "0.3.5"
}

Mise à jour du fichier de configuration

Il est maintenant nécessaire d’appeler la librairie dans le fichier de configuration :

var HtmlScreenshotReporter = require('protractor-jasmine2-screenshot-reporter');

var reporter = new HtmlScreenshotReporter({
    dest: '../reports',
    filename: 'reporting-' + getDate(true) + '.html',
    reportOnlyFailedSpecs: false,
    captureOnlyFailedSpecs: true,
    showQuickLinks: true,
    reportTitle: "Rapport de tests automatisés du " + getDate() + "",
    reportFailedUrl: true,
    pathBuilder: function(currentSpec, suites, browserCapabilities) {
        return currentSpec.fullName + "--" + browserCapabilities.get('browserName');
    }
});

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['../specs/dis-bonjour.js'],
    baseUrl: 'https://angularjs.org',
    framework: 'jasmine2',

    beforeLaunch: function() {
        return new Promise(function(resolve){
            reporter.beforeLaunch(resolve);
        });
    },

    onPrepare: function() {
        jasmine.getEnv().addReporter(reporter);
    },

    afterLaunch: function(exitCode) {
        return new Promise(function(resolve){
            reporter.afterLaunch(resolve.bind(this, exitCode));
        });
    }
};

function getDate(dansNomFichier) {
    var today = new Date();
    var dd = today.getDate();
    var mm = today.getMonth() + 1;
    var yy = today.getFullYear();
    var hh = today.getHours();
    var min = today.getMinutes();

    if(dd<10) dd='0' + dd;
    if(mm<10) mm='0' + mm;
    if(hh<10) hh='0' + hh;
    if(min<10) min='0' + min;

    if(dansNomFichier){
        today = yy + '-' + mm + '-' + dd + '--' + hh + 'h' + min;
    } else {
        today = yy + '/' + mm + '/' + dd + ', ' + hh + ':' + min;
    }
    return today;
}

Explication du fichier de configuration

dest: '../reports'

Avec cette configuration de la variable dest, si votre fichier de configuration se trouve bien dans un sous-dossier dédié (/src/confs par exemple), un sous-dossier « reports » sera créé dans le dossier « src ». A l’intérieur de ce dossier seront créés les reportings au format html, ainsi que les screenshots.

A chaque lancement, son contenu sera supprimé. Si vous souhaitez conserver les résultats des tests d’un lancement à l’autre, mettez à true la variable preserveDirectory lors de l’instanciation de votre HtmlScreenshotReporter :

preserveDirectory: true,

Dans ce cas, un sous-dossier avec un nom aléatoire sera créé à l’intérieur du dossier « reports ».

filename: 'reporting-' + getDate(true) + '.html',

La variable filename, sans surprise, définit le titre du fichier de reporting ; ici, nous avons choisi de lui ajouter la date et l’heure (voir la fonction « getDate » en bas du fichier).

reportOnlyFailedSpecs: false,
captureOnlyFailedSpecs: true,

Les variables reportOnlyFailedSpecs et captureOnlyFailedSpecs permettent de consigner tous les cas de tests dans le reporting, mais de ne prendre un screenshot qu’en cas d’échec.

showQuickLinks: true,

La variable showQuickLinks est à false par défaut. Ici, elle permet de générer au début du fichier des liens vers chacun des cas de test en échec. Pratique quand on se retrouve avec une très grande campagne de test.

reportTitle: "Rapport de tests automatisés du " + getDate() + "",

La variable reportTitle configure le titre qui s’affiche en haut du rapport html.

reportFailedUrl: true,

La variable reportFailedUrl permet d’afficher dans le reporting l’URL sur laquelle l’échec a eu lieu.

pathBuilder: function(currentSpec, suites, browserCapabilities) {
    return currentSpec.fullName + "--" + browserCapabilities.get('browserName');
}

La fonction pathBuilder permet de définir le nom des fichiers de screenshot. Ici, le nom est composé du titre du cas de test et du nom du navigateur.

Notre série sur Protractor est maintenant terminée. En espérant qu’elle vous aura apporté des éléments, nous sommes curieux d’avoir vos retours sur ce framework.

Pour en savoir plus sur les différents outils de tests automatisés ainsi que leurs usages et leurs bonnes pratiques, rendez-vous ici !

Protractor + AngularJS : installation et exemple de script

Note préalable : cet article date de 2017. Depuis, Protractor a atteint la fin de sa vie. Merci à lui et surtout aux personnes qui ont contribué à cet outil !

Pourquoi Protractor ?

Protractor est un framework de test dont le but est de faciliter l’automatisation des tests fonctionnels pour les SUT (system under test) développés avec AngularJS. Il se base sur WebdriverJS, l’implémentation Javascript de Selenium, et intègre des solutions telles que Jasmine, qui permet de rédiger des tests en BDD (behavior-driver development), ou encore Cucumber et Mocha.

Protractor reconnaît les concepts AngularJS, tels que les attributs ng-repeaterng-controllerng-model… qui sont employés dans le code HTML pour manipuler les composants web.

D’autres frameworks de test sont bien sûr capables d’automatiser les applications AngularJS, mais utiliser Protractor améliore :

  • la rapidité d’écriture des tests
  • leur maintenabilité (passer par des xpath ou des sélecteurs CSS serait moins robuste)
  • leur lisibilité

Inversement, il est possible d’utiliser Protractor pour automatiser les tests d’une application non-AngularJS (voir cet article).

Nous vous proposons donc une série d’articles sur Protractor :

  1. Installation et exemple de script (le présent article !)
  2. Gestion des jeux de données et des dépendances du projet
  3. Structure d’un projet industrialisé

Nous utilisons ici la version 5 de Protractor.

Installation de Protractor

Voici quelques prérequis avant de pouvoir vous lancer dans les tests avec Protractor :

  • Avoir NodeJS installé sur sa machine (ce qui permet d’utiliser npm)
  • Pour l’installation, lancer les commandes suivantes :

npm install -g protractor
webdriver-manager update

  • Pour démarrer le serveur Selenium sur votre machine, démarrer une invite et lancer la commande :

webdriver-manager start

EDIT de décembre 2020 : Si à ce moment-là votre invite de commande vous indique que « java n’est pas reconnu en tant que commande interne ou externe », assurez-vous que votre PATH contient bien le chemin vers le dossier « bin » qui se trouve dans le dossier où est installé Java. Ca peut ressembler à C:Program FilesJavajdk-xx.x.xbin. (Fin de l’EDIT)

Par défaut, le serveur Selenium est lancé sur le port 4444. Vous pouvez changer le port en ajoutant un paramètre à la commande.

webdriver-manager start –seleniumPort 5555

Tant que vous utiliserez Protractor sur votre machine, ne fermez pas cette invite.

Pour vérifier que le serveur est bien lancé, rendez-vous sur http://localhost:4444/wd/hub (en changeant le port si besoin).

Exemple de test Protractor

Au cours de ce test, nous allons interagir avec un composant de la page d’accueil du site d’AngularJS.

Sur cette page, quand l’utilisateur entre une chaîne de caractères dans le champ « Name », la phrase de salutations se met immédiatement à jour. C’est cette fonctionnalité que nous allons tester.

Dans l’inspecteur, on voit que l’attribut « ng-model » du champ de saisi a pour valeur « yourName » ; c’est cet attribut qui va nous servir lors de l’identification de l’objet.

Nous avons maintenant besoin de 2 fichiers : le fichier de configuration, qui permet de lancer le test, et le script de test en lui-même.

Un script de test Protractor

Le fichier de test (appelons-le dis-bonjour.js) a le contenu suivant :

describe("Suite de test de la feature qui dit bonjour", function() {

  it("Test 1 : si je renseigne mon nom, la feature me dit bonjour", function() {
    browser.get(browser.baseUrl);
    element(by.model('yourName')).sendKeys('Zoé');
    var message= element(by.xpath("//div[contains(@class, 'well')]//h1"));
    expect(message.getText()).toEqual('Hello Zoé!');
  });

  it("Test 2 : si je rêvasse en écrivant mon script de test, j'obtiens un faux positif", function() {
    browser.get('https://angularjs.org');
    element(by.model('yourName')).sendKeys('Chloé');
    var message= element(by.xpath("//div[contains(@class, 'well')]//h1"));
    expect(message.getText()).toEqual('Hello Zoé!');
  });

});

EDIT de décembre 2020 : si vous souhaitez lancer vos tests sur une application qui n’est pas une application Angular, il est important d’intégrer la commande suivante à vos scripts, afin qu’elle empêche Protractor d’attendre qu’Angular soit correctement chargé : browser.waitForAngularEnabled(false). (Fin de l’EDIT)

Structure et la syntaxe du script Protractor

Conformément à la syntaxe Jasmine, la suite de test se déroule dans la fonction « describe ». Chaque test est détaillé dans une fonction « it ».

L’URL de base (browser.baseUrl) est définie dans le fichier de configuration. Rien n’empêche bien sûr d’utiliser une autre URL (comme par exemple dans le deuxième test du fichier).

Fichier de configuration Protractor

Et maintenant, le fichier de configuration qui va permettre de lancer ces deux courts tests. Appelons-le conf.js. Celui-ci est minimaliste et ne contient que l’adresse du serveur Selenium, le chemin du script de test, et l’URL du SUT.

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['dis-bonjour.js'],
  baseUrl: 'http://monsupersiteweb.nc'
};

Lancer les tests Protractor en ligne de commande

Maintenant, pour lancer les deux tests contenus dans le script, il suffit d’ouvrir une invite de commande, de se positionner dans le dossier contenant les deux fichiers et de lancer la commande :

protractor conf.js

Remarque : il est possible de changer la baseUrl au moment du lancement du test, ce qui donne :

protractor --baseUrl=http://monsupersiteweb.nc conf.js

Ce qui est pratique si l’on souhaite faire tourner les tests sur différents environnements par exemple.

Par défaut, c’est un navigateur Google Chrome qui s’ouvre et exécute les scénarios définis dans le script de test Protractor. Ce qui donne, dans notre cas, le résultat suivant :

Plutôt clair, non ?

Protractor et les IDE

Pour l’écriture des tests, un simple éditeur de texte suffit pour si l’on veut juste tester les commandes de bases et se faire une idée rapide des possibilités du framework. Mais pour industrialiser un projet de tests automatisés, il est bien sûr préférable d’utiliser un IDE. Il existe des plugins Protractor pour IntelliJ et pour Eclipse. Il est à noter que celui d’IntelliJ n’est disponible que si l’on dispose d’une licence commerciale (version Ultimate, et non Community). Il est possible d’essayer gratuitement la version Ultimate pendant 30 jours.

Protractor avec Eclipse

EDIT de décembre 2020 : le bug évoqué est peut-être résolu depuis, nous n’avons pas réessayé. (Fin de l’EDIT)

Configurer le plugin Protractor d’Eclipse nécessite quelques manipulations en plus (détaillées dans cette vidéo). Attention toutefois, en mars 2017 un bug empêchait le plugin de tourner correctement.

Une erreur s'affiche : "SyntaxError: Unexpected token ..."

En effet, la configuration de Run Protractor utilise par défaut la version 4.2.4 de NodeJS, alors que 2 versions majeures ont été publiées depuis !

Indiquer un chemin vers un exécutable plus récent est possible, mais provoque une erreur : « Error while launching client file, Cannot find node install path node-native ».

A suivre donc… Pour la suite de cette série d’articles, nous utiliserons donc la solution d’évitement : passer sur la version d’essai d’IntelliJ.

Protractor avec IntelliJ

  1. Créez un projet.
  2. Dans le fichier src, placez vos 2 fichiers (test et configuration).
  3. Créer une « Run configuration » Protractor
  4. Renseigner le chemin vers le fichier de configuration. Les chemins vers NodeJS et Protractor sont, normalement, trouvée automatiquement.

Voilà ce à quoi ressemblent les résultats d’exécution :

Protractor avec Visual Studio Code

Ce paragraphe est un EDIT de décembre 2020.

Ouvrez une nouvelle fenêtre de Visual Studio code ; dans la partie de droite de l’écran s’affiche un encart appelé « Customize ». Dans la partie « Tools and languages », faites en sorte que VSC supporte JavaScript, TypeScript, et si vous le sentez vous pouvez également installer d’autres extensions telles que Mocha Snippets ou Protractor Snippets, qui proposent des raccourcis permettant d’accélérer la saisie des scripts de test Protractor.

Cette manipulation décuplera votre productivité par l’autocomplétion !

(Fin de l’EDIT)

On se retrouve demain pour aller plus loin dans l’exploration de Protractor. Car on ne veut pas seulement créer un test, on veut pouvoir le jouer avec une multitude de jeux de données !

Protractor + AngularJS : Gestion des jeux de données et des dépendances du projet

Note préalable : cet article date de 2017. Depuis, Protractor a atteint la fin de sa vie. Merci à lui et surtout aux personnes qui ont contribué à cet outil !

Le problème du tas

Au commencement d’un projet de test, surtout si celui est censé n’être qu’un POC ou un tout petit projet, on a tendance à coder les jeux de données en dur dans le script et à télécharger les librairies au fil de l’eau. Trèèès bieeeen… tant que ça ne dure pas ! Car comme écrivait Beckett en 1957 :

Les grains s’ajoutent aux grains, un à un, et un jour, soudain, c’est un tas, un petit tas, l’impossible tas.

Un tas qu’il n’est pas facile à gérer quand on revient sur le projet quelques mois plus tard, ou qu’un collègue doit travailler dessus pendant qu’on est en vacances à Ouvéa.

Aujourd’hui donc, on vous donne des pistes pour gérer les jeux de données et les librairies de votre projet. Cet article est le deuxième de notre série sur les test Protractor :

  1. Installation et exemple de script
  2. Gestion des jeux de données et des dépendances du projet
  3. Structure d’un projet industrialisé

Quels problèmes souhaitons-nous éviter ?

Pour comprendre l’intérêt de ce qui va suivre, penchons-nous sur ce petit texte :

« Vous a-t-on déjà parlé de la Nouvelle Calédonie, un archipel du Pacifique dont la capitale est Nouméa, qui compte quelque 300 000 habitants et dont la devise est le franc pacifique ? Recherchez le taux de change actuel entre le franc pacifique et l’euro. La Nouvelle Calédonie, un archipel du Pacifique dont la capitale est Nouméa, qui compte quelque 300 000 habitants et dont la devise est le franc pacifique, possède un lagon exceptionnel, classé au patrimoine mondial de l’Unesco. Recherchez depuis quand c’est le cas. L’oiseau fétiche de la Nouvelle Calédonie, un archipel du Pacifique dont la capitale est Nouméa, qui compte quelque 300 000 habitants et dont la devise est le franc pacifique, est le cagou, un oiseau à huppe presque incapable de voler. Trouvez d’où lui vient son nom. Il est très agréable de vivre en Nouvelle Calédonie, un archipel du Pacifique dont la capitale est Nouméa, qui compte quelque 300 000 habitants et dont la devise est le franc pacifique, cependant il faut tenir compte du fait que ce coin de paradis se trouve à quelque 16 000 kilomètres de la métropole. Vérifiez que Nouméa et la ville française la plus éloignée de Paris. »

Du point de vue du lecteur, ce texte est totalement redondant et illisible. Vous auriez préféré ne lire chaque information qu’une seule fois, et votre cerveau aurait rattaché chacune de ces données à la variable « Nouvelle Calédonie », qu’il possédait peut-être déjà. Cela vous aurait permis de vous concentrer davantage sur les questions.

Si l’on se place du point de vue de l’auteur, au prochain recensement de la Nouvelle Calédonie, il faudra qu’il remplace chaque occurrence de « 300 000 » par, peut-être, « 320 000 » ou « 350 000 ». Au fil du temps, le texte s’allongeant les mises à jour deviendront de plus en plus fastidieuses.

Ce texte est comme un projet de test mal architecturé, qui en un seul bloc fournit une multitude d’informations souvent répétitives sur le SUT, et sollicite dans la foulée des vérifications.

La bonne pratique

Autant que possible, nous allons donc prendre soin de séparer les moments où :

  • on configure les données du test,
  • on donne des informations sur le SUT (identificateurs d’objets, descriptions de page),
  • on effectue des vérifications (tel élément est présent, tel texte est visible).

Gestion des données de test avec jasmine-data-provider

En automatisation des tests, une bonne pratique consiste à séparer les données du test et les scripts d’exécution. Cela augmente la maintenabilité des tests et permet aussi à tout un chacun de mettre à jour le jeu de données, sans avoir à connaître l’architecture du projet sur le bout des doigts.

Qui plus est, cela permet d’alimenter un même script avec X jeux de données, et d’augmenter la couverture des tests à moindre coût .

Avec Protractor, le plugin jasmine-data-provider par exemple permet d’implémenter cette séparation données/scripts.

Pour l’utiliser dans l’exemple précédent, nous allons suivre trois étapes.

1) Importer jasmine-data-provider

A la racine de votre projet, téléchargez le package jasmine-data-provider :

npm install jasmine-data-provider --save-dev

Un dossier node_modules va se créer à la racine de votre projet et contenir le paquet voulu.

Il existe une autre manière de procéder, que nous verrons plus tard. En attendant, veillez juste à ne pas répertorier le dossier node_modules dans votre outil de versionning. Par exemple, si vous utilisez Git, ajoutez la ligne « node_modules/ » dans votre fichier .gitignore et votre projet restera léger comme une plume.

2) Modifier le fichier de configuration

Ici, il n’y a qu’une ligne à ajouter :

framework: 'jasmine2'

3) Créer un fichier de données

Ce fichier doit être au format .js. Appelons-le jeux-de-donnees-demo.js.

Ici, nous avons 2 séries de jeux de données. La première comprend 5 jeux de données avec chacun 1 paramètre : le prénom. Outre les caractères spéciaux courants dans la langue française, nous voulons savoir si d’autres caractères spéciaux (ici, arabes et chinois) apparaissent correctement, et si les scripts Javascript sont traités comme du simple texte.

La deuxième série, destinée à générer un faux positif,  a 2 paramètres : le prénom saisi et le prénom attendu.

'use strict';

 module.exports = {
    prenoms: {
        'ASCII': { prenom: 'Robert' },
        'Caractères spéciaux français': { prenom: 'Rôbértà' },
        'Caractères spéciaux arabes': { prenom: 'صباح الخير' },
        'Caractères spéciaux chinois': { prenom: '安' },
        'Script': { prenom: "<script type='text/javascript'>alert('Faille 1');</script>"}
    },

    faux_positif: {
        'Fail !': { prenomSaisi: 'Chloé', prenomVoulu: "Zoé" }
    }
}

La mention « use strict » est facultative mais conseillée, car elle garantit que le code est bien formé.

4) Exploiter le fichier de données au sein du script de test Protractor

Nous allons un peu transformer le script de l’article précédent :

var donnees = require('./jeu-de-donnees-demo.js');
var using = require('jasmine-data-provider');

describe("Suite de test de la feature qui dit bonjour", function() {

    beforeEach(function () {
        browser.get(browser.baseUrl);
    });

    using(donnees.prenoms, function (data, description) {
        it("Chaîne de type " + description, function () {
            element(by.model('yourName')).sendKeys(data.prenom);
            var message= element(by.xpath("//div[contains(@class, 'well')]//h1"));
            expect(message.getText()).toEqual('Hello ' + data.prenom + '!');
        });
    });

    using(donnees.faux_positif, function(data, description) {
        it("Si je rêvasse en écrivant mon script de test, j'obtiens un faux positif", function() {
            element(by.model('yourName')).sendKeys(data.prenomSaisi);
            var message= element(by.xpath("//div[contains(@class, 'well')]//h1"));
            expect(message.getText()).toEqual(data.prenomVoulu);
        });
    });
});

Au début du script, on indique que l’on souhaite utiliser jasmine-data-provider, ainsi que le chemin de notre fichier de données.

Pour le premier cas de test, on déclare qu’on va utiliser la série de données intitulée « prenoms », ce qui nous donne accès aux données (« data ») ainsi qu’au nom de chaque jeu de données (« description »). Pour accéder au paramètre « prenom », il suffit d’indiquer « data.prenom ».

Nous avons également choisi de factoriser, entre les deux cas de test, la première étape, à savoir la consultation de l’URL de base du projet :

beforeEach(function () {
    browser.get(browser.baseUrl);
});

Au lancement, la même méthode de test sera lancée autant de fois qu’il y a de jeux de données, avec les paramètres correspondants.

Gestion des données de test avec csv-parse

NB : cette section a été ajoutée en décembre 2020.

Si vous préférez exploiter des fichiers CSV, c’est possible aussi en utilisant le module csv-parse.

Commencez par installer ce module avec la ligne de commande suivante :

npm install csv-parse

Dans cet exemple, nous utiliseront un fichier de jeu de données nommé donnees.csv, et qui contiendra les informations suivantes :

login,password,prenom
admin,M0t2p@sseAdmin,Amira
user,M0t2p@sseUser,Boniface

Nous allons appeler ce jeu de données dans le script suivant, qui vérifie qu’à l’authentification sur un certain site web, on se retrouve sur une page où l’on voit la mention « Bonjour » suivie du prénom de l’utilisateur.

'use strict';
let fs = require('fs'); // fs pour file system. Il n'y a rien besoin d'installer pour utiliser cela.
const parse = require('csv-parse/lib/sync');
let a = fs.readFileSync('donnees.csv');

const testdata = parse(a, {
    columns: true,
    skip_empty_lines: true
})

describe('Suite de tests d'authentification', function () {
    for (let i of testdata) {       
        it('Connexion et affichage de la page d'accueil', async function () {
            browser.get(browser.baseUrl)
            console.log("Saisie du login : " + i.login)
            element(by.id('input-login-username')).sendKeys(i.login)
            console.log("Saisie du mot de passe : " + i.password)
            element(by.id('input-login-password')).sendKeys(i.password)
            element(by.id('button-login-submit')).click()
            console.log("Vérification de l'affichage des salutations")
            var message= element(by.xpath("//div[contains(@class, 'h1')]"))
            expect(message.getText()).toEqual('Bonjour ' + i.prenom)
        });
    }
});

Et voilà !

(Fin de l’EDIT)

Gestion des dépendances avec le package.json

Nous venons de voir comment rapidement ajouter la dernière version de jasmine-data-provider. Mais cette façon de procéder n’est pas la meilleure au sein d’un projet industrialisé, où certaines problématiques se posent :

  • Comment alléger le projet de tests Protractor ?
  • Comment faire en sorte que tous les membres de l’équipe utilisent la même version de jasmine-data-provider ?

C’est à ce moment qu’intervient le fichier package.json. Ce fichier, que l’on va placer à la racine du projet, contient une liste de tous les packages dont a besoin le projet. Si vous venez de l’univers Maven, cela devrait vous rappeler le pom.xml, ou le build.gradle si vous venez de la constellation Gradle.

Et la bonne nouvelle, c’est que npm propose une façon rapide de générer un fichier package.json ! Pour ce faire, placez-vous à la racine de votre projet, et tapez la commande :

npm init

Une série de questions sur votre projet vous est posée. Des réponses par défaut vous sont proposées. Pour un petit projet tel que nous venons d’en créer, l’opération dure moins d’une minute.

La valeur entrée pour le « main » dans l’utilitaire n’importe pas du tout ici. Vous pourrez la supprimer de votre package.json.

Pour le « test command », entrez la commande suivante :

node_modules/.bin/protractor src/conf-demo.js

A ce stade, le fichier généré ressemble à cela :

{
  "name": "hightest-demo",
  "version": "1.0.0",
  "description": "Un petit projet de démo pour présenter les tests Protractor",
  "dependencies": {
    "jasmine-data-provider": "^2.2.0"
  },
  "devDependencies": {},
  "scripts": {
    "test": "protractor src/conf-demo.js"
  },
  "author": "",
  "license": "ISC"
}

L’utilitaire a automatiquement détecté le package jasmine-data-provider, et l’a donc déclaré dans le package.json. Vous avez remarqué ce petit accent circonflexe juste avant le numéro de version ? Il fait partie de la syntaxe de versionning de npm (que nous vous invitons à découvrir ici) et permet ici de bénéficier des mises à jour non-majeures du paquet.

Cependant, nous vous conseillons autant que possible de fixer les versions de vos projets, afin d’être sûr d’avoir des configurations similaires d’une machine à l’autre.

Vérifions que tout marche bien : supprimons sauvagement le dossier node_modules créé précédemment, et lançons la commande « npm install » à la racine du projet. Et hop, le dossier réapparaît.

De bonnes fondations sont posées, mais il reste encore quelques éléments à prendre en compte pour que le projet soit industrialisable. A demain pour aller encore plus loin avec votre projet de test Protractor !

Découvrez nos autres séries d’articles !

Notre saga du test audiovisuel

Notre série dédiée aux tests de sécurité

Le kit de secours métaphorique du testeur agile