Introduction à Nightwatch, pour des tests d'acceptance ultra rapides

Depuis quelques mois, les développeurs se demandent un peu quoi utiliser pour leurs tests d’acceptance dans les nouveaux projets. CasperJS n’ayant pas sorti de nouvelle version depuis longtemps et Protractor n’ayant jamais vraiment convaincu pour les projets angular, l’horizon semblait quelque peu désert en terme de solution solide. Mais ce n’était pas sans compter sur la communauté qui a créé Nightwatch (non, c’est pas les mêmes que dans Game Of Thrones …)

Présentation

logo-nightwatchNightwatch est une surcouche à Selenium pour Node.js permettant de piloter ce dernier grâce au protocole Selenium JsonWireProtocol via une API simple et élégante.
Voici une illustration représentant assez bien le fonctionnement de Nigtwatch avec Selenium puis le navigateur :
How Nightwatch works
Nightwatch, se distingue de la concurrence de plusieurs façons :

  • Comme dit précédemment sa syntaxe reste assez classe
  • Il est facile à étendre (nous verrons d’ailleurs comment dans la suite de cet article)
  • Intègre de base le pattern de Page Object
  • Avec selenium sous le capot, vous profiter de l’éco-système de cette plateforme. Je pense notamment aux outils Sass comme BrowserStack ou SauceLabs et toute la suite des webdriver qui permettent de se brancher à tous les navigateurs.
  • Mais tout ceci n’est rien comparé à ce qui est pour moi est une Killer Feature : Nightwatch est capable de paralléliser l’exécution de vos suites de test !

Show me the code !

Getting started

Pour les explications qui vont suivre nous partirons avec l’arborescence du projet nightwatch-boilerplate créé pour l’article, décrite ici :
├── nightwatch.json
├── nightwatch.globals.js
├── nw
│       ├── reports
│       ├── logs
│       └── tests
└── package.json
Si vous voulez la solution déjà complète, je vous conseil de cloner directement ce repository.
Comme d’habitude, nous commençons par l’installation de paquets (à la racine du projet) :

npm install nightwatch selenium-server-standalone-jar chromedriver --save-dev

Ici, seule la première dépendance est vraiment obligatoire, mais afin de vous faire gagner du temps, nous installons aussi un package contenant le selenium-server (qui se présente sous la forme d’un simple fichier jar), ainsi qu’un autre contenant le ChromeDriver.

L’étape suivante est de créer le fichier de configuration nightwatch nommé nightwatch.json. Voici l’exemple de fichier sur lequel nous allons travailler (j’explique chaque champs après):

{
  "src_folders" : ["nw/tests"],
  "output_folder" : "nw/reports",
  "globals_path" : "nightwatch.globals.js",
  "test_workers": {
    "enabled": true,
    "workers": "auto"
  },
  "selenium" : {
    "start_process" : true,
    "server_path" : "./node_modules/selenium-server-standalone-jar/jar/selenium-server-standalone-2.50.1.jar",
    "log_path" : "nw/logs",
    "host" : "127.0.0.1",
    "port" : 4444,
    "cli_args" : {
      "webdriver.chrome.driver" : "./node_modules/chromedriver/bin/chromedriver",
      "webdriver.ie.driver" : ""
    }
  },
  "test_settings" : {
    "default" : {
      "launch_url" : "http://google.com",
      "selenium_port"  : 4444,
      "selenium_host"  : "localhost",
      "silent": true,
      "screenshots" : {
        "enabled" : true,
        "path" : ""
      },
      "desiredCapabilities": {
        "browserName": "chrome",
        "javascriptEnabled": true,
        "acceptSslCerts": true
      }
    },
    "french" : {
      "launch_url" : "http://google.fr",
      "desiredCapabilities": {
        "browserName": "firefox",
        "javascriptEnabled": true,
        "acceptSslCerts": true
      }
    }
  }
}
  • src_folders : Dossiers où trouver les suites de test.
  • output_folders : Dossier où créer les rapports de test.
  • test_workers : Est la partie où on définie si l’on veut que nos suites de test soient parallélisées. Ici on définie qu’elles le seront “enabled:true”, et que le nombre d’execution sera en fonction du nombre de CPU “workers:true”.
  • globals_path : Fichier des paramètres globaux des suites de test sur lequel nous reviendrons un peu plus loin.
  • selenium : Cette partie définie la configuration générale pour se brancher au server Selenium. Nous nous ne attarderons pas dessus, mais notez quand même que nous avons précisé les chemins pour accéder au serveur Selenium et au Chrome driver.
  • test_settings : Cette partie, est probablement la plus intéressante puisque qu’elle va vous permettre de définir vos environnements de test. Dans cette exemple nous définissons les paramètres de l’environnement par default et celui de l’environnement “french”. A savoir que les environnement supplémentaires héritent de la configuration par default, c’est pour cela que nous n’avons eu qu’à “surcharger” l’url cible de notre environnement (“launch_url”) et le navigateur avec (“desiredCapabilities”)

Le bloc desiredCapabilities peut paraitre un peu déstabilisant pour juste configurer le navigateur qu’on veut lancer. En vérité, ce format est directement issue de la configuration Selenium et permet de configurer une multitude de chose dans le navigateur cible. Pour en savoir plus, je vous conseille la doc officielle

Premier test

Il est temps d’attaquer notre premier test, on créé le fichier nw/tests/reseachOnGoogle.test.js qui contient ceci :

module.exports = {
    'Search on google': (browser) => {
        browser
            .init()
            .waitForElementVisible('body', 1000)
            .setValue('input[type=text]', 'nightwatch')
            .waitForElementVisible('button[name=btnG]', 1000)
            .click('button[name=btnG]')
            .pause(1000)
            .assert.containsText('#main', 'Night Watch')
            .end()
    },
    after: (browser) => {
        browser.end()
    }
};

Ce fichier décrit une suite de test, contenant un test ‘Search on google’. En premier, ce test commence par un init qui à pour but de se rendre sur la page, puis attend qu’un élément soit visible avec waitForElement, met une valeur dans le champ de recherche avec setValue, clique sur le bouton de recherche avec click (en ayant vérifié avant qu’il était là), attend une seconde avec pause. Puis la fonction ‘after’ appelée automatiquement à la fin de la suite de test par Nightwatch appelle la méthode end.

L’appel de la méthode end est très important, car elle est responsable de la fermeture du navigateur utilisé pour faire le test. Je vous conseille donc de l’appeler à chaque fin de suite de test avec le after.

Il ne reste plus qu’à le lancer avec la commande node_modules/nightwatch/bin/nightwatch -c nightwatch.json
runTest3
Nous avons un résultat semblable à la fenêtre ci-dessus ainsi qu’un Chrome qui s’ouvre en arrière plan où l’on voit les manipulations demandées. Vous voulez lancer l’environnement ‘french’ maintenant ? Rien de plus simple :
node_modules/nightwatch/bin/nightwatch -c nightwatch.json –env french
Aucune différence à la console, par contre c’est un Firefox qui est lancé maintenant et qui se rend sur “http://google.fr” au lieu de “http://google.com” de l’environnement par default.

Les “globals” pour simplifier le tout

Le fichier globals va être bien utile pour simplifier les tests en définissant des paramètres globaux. Par exemple, actuellement, nous avons besoin de définir systématiquement la durée maximum d’attente pour trouver un élément CSS. Ce serait bien pratique de pouvoir définir un temps d’attente global, c’est justement le genre de possibilités offertes par ce fichier, en plus de pouvoir définir des hooks à différent moment du test et de définir des variables d’environnement dynamiques (à la différence des variables statiques du fichier de configuration “déclaratif” json de nightwatch).
Voici le fichier que nous allons utiliser, les commentaires vous permettrons de comprendre sa structure :

module.exports = {
    default: { // Paramètres de l'environement 'default'
        searchTerm: 'nightwatch',
        movieName: 'Night Watch'
    },
    french: { // Paramètres de l'environement 'french'
        searchTerm: 'dikkenek',
        movieName: 'dikkenek'
    },
    // Arrête tout dès qu'un test échoue
    abortOnAssertionFailure: true,
    // Délais entre deux vérifications
    waitForConditionPollInterval: 300,
    // Délais à attendre par défault
    waitForConditionTimeout: 1000,
    // Echoue si une selection retourne plusieurs éléments alors qu'elle devait n'en retourner qu'un
    throwOnMultipleElementsReturned: false,
    // Avant et après l'éxecution de l'ensemble des tests
    before: (next) => next(),
    after: (next) => next(),
    //  Avant et après chaque éxecution de suite des tests
    beforeEach: (browser, next) => next(),
    afterEach: (browser, next) => next(),
    // Pour customiser le reporter de test
    reporter: (results, next) => next()
};

Ce qui nous permet de réfactorer la suite de test comme suit :

module.exports = {
    'Search on google': (browser) => {
        browser
            .init()
            .waitForElementVisible('body')
            .setValue('input[type=text]', browser.globals.searchTerm)
            .waitForElementVisible('button[name=btnG]')
            .click('button[name=btnG]')
            .pause(1000)
            .assert.containsText('#main', browser.globals.movieName)
            .end()
    },
    after: (browser) => {
        browser.end()
    }
};

Vous remarquerez l’utilisation des variables ‘globals’ dynamiques définies pour chaque environnement. Elles sont bien pratiques quand vous avez besoin par exemple de passer une donnée sensible accessible uniquement en variable d’environnement.

Et en parallèle ça donne quoi ?

Comme notre configuration lance déjà les suites de test en parallèle, il nous faut juste ajouter une deuxième suite de test pour voir l’exécution s’effectuer en même temps. Créons donc une suite nw/tests/nightWatchIsMovie.test.js comme ceci :

module.exports = {
    'Go To google': (browser) => {
        browser
            .init()
            .waitForElementVisible('body')
            .setValue('input[type=text]', browser.globals.searchTerm)
            .waitForElementVisible('button[name=btnG]')
            .click('button[name=btnG]')
            .pause(1000)
    },
    'Check movie name': (browser) => {
        browser
            .assert.containsText('.mod .kno-ecr-pt.kno-fb-ctx', browser.globals.movieName)
            .assert.containsText('.mod ._gdf', '2004')
    },
    after: (browser) => {
        browser.end();
    }
};

Petite différence avec ce nouveau fichier, on a plusieurs tests qui composent notre suite. On ré-éxecute les tests et, TADA !
run test in parallel nightwatch
Vous avez deux suites de test lancées en parallèle dans deux chromes différents (sous réserve que vous avez au moins deux coeurs/cpu, ce qui est le cas je l’espère).

Bilan & Suite

Avec une configuration pas si méchante que ça, Nightwatch est un outil très facile à prendre en main. En effet, la configuration minimale est rapide à mettre en oeuvre, les suites de test restent lisibles et maintenables et sa fonctionnalité de parallélisation des suites fait gagner un temps monstrueux par rapport au temps habituel pour exécuter ce genre de tests !
En bonus, les résultats des tests sont lisibles par le développeur dans le terminal et par votre système d’intégration continue grâce aux sorties xUnit présentes dans le dossier nw/reports.
Dans un prochain article nous traiterons des Pages Object et de comment développer des commands complexes et autre tips sur Nightwatch.
Stay tuned…
Par Mathieu Breton CTO chez JS-Republic.