Publié par
Il y a 5 années · 10 minutes · Java / JEE

Intégrer ses tests JavaScript dans Grails

Grails est un framework web permettant de développer rapidement en Groovy des applications modernes. Mais qui dit "applications web modernes" dit également "JavaScript", beaucoup de JavaScript. Il devient donc vite nécessaire de tester le code écrit en JS, de façon à trouver rapidement les nombreuses régressions qui ne manqueront pas d’arriver au cours de la vie de l’application. Dans cet article, je vous propose une façon d’intégrer des tests JavaScript dans votre projet Grails, mais surtout de voir comment les lancer simplement depuis le shell Grails avec la commande test-app :js

Le mécanisme de test de Grails

Grails fournit un utilitaire en ligne de commande permettant de gérer le cycle de vie de l’application. On peut, par exemple, lancer l’application en local avec la commande grails run-app ou encore, et c’est le cas qui va nous intéresser, exécuter les test via la commande grails test-app.

La commande test-app possède de nombreuses options, en particulier celles qui permettent de ne lancer que les tests unitaires ou que les tests d’intégration, ou pour aller plus loin, de n’exécuter les tests que sur un fichier ou un package. Par exemple :

# N'execute que les test unitaires des fichiers commencant par S
grails test-app unit: S*
# N'execute aussi que les tests unitaires
grails test-app :unit S*
# N'execute toujours que les tests unitaires
grails test-app unit:unit S*

En première approche, il peut sembler étrange d’avoir 3 syntaxes différentes pour faire la même chose. En réalité, chacune des commandes ci-dessus effectue une action légèrement différente. 

La syntaxe attendue par la commande test-app est la suivante : grails test-app <phase>:<type> <pattern>

La phase représente le contexte d’exécution des tests

  • unit : l’application n’est pas démarrée ;
  • integration : l’application est démarrée, les dépendances injectées, mais il n’y pas d’interface HTTP ;
  • functionnal: l’application est démarrée comme par la commande run-app, et peut être attaquée directement par requêtes HTTP.

Les types de test sont déclarés comme pouvant fonctionner dans une phase donnée. Par défaut, les types fournis sont

  • unit : test unitaire de JUnit, executable en phase unit ;
  • integration : test d’integration de Junit, executable en phase integration.

Une intégration élégante de test unitaire JavaScript, serait donc d’avoir la possiblité de lancer les commandes suivantes

#Execute tous les tests de la phase unit, groovy ET Javascript, commencant par S
grails test-app unit: S*
#Execute les tests groovy commencant par S
grails test-app :unit S*
#Execute les tests js commencant par S
grails test-app :js S*

En pratique, on souhaite donc ajouter le type js à la phase unit. Pour cela, il va falloir éditer un fichier nommé _Events.groovy à mettre dans le répertoire scripts de l’application. Ce fichier de script permet d’écouter les événements envoyés par les scripts grails au cours de leur exécution.

Malheureusement, la documentation associée à ce fichier étant assez succinte, il n’est pas toujours évident de réussir à faire ce que l’on désire. C’est pourquoi nous allons voir comment mettre en place nos tests unitaires JavaScript dans le chapitre suivant.

Mise en pratique

En remarque préalable, la version de Grails utilisée ici est la 2.2.1. Le principe pour les versions antérieures est le même et devrait pouvoir être adapté facilement. 

Pour l’exemple, nous choisirons ici QUnit pour réaliser nos tests unitaires JavaScript. QUnit a pour lui l’avantage de son extrême simplicité, et est donc tout indiqué pour un exemple simple. Ici encore, le code devrait être simple à transposer pour être utilisé avec un autre framework.

Un test QUnit est composé de :

  • une page HTML chargeant les données nécessaires aux tests (scripts, fixtures, mock, etc.) ;
  • un fichier JavaScript contenant les tests.

Exemple de test QUnit

La mise en place de QUnit peut se faire de la façon suivante :

  • télécharger QUnit sur le site du projet ;
  • créer un répertoire unit-js dans le répertoire test de Grails ;
  • créer un répertoire vendor dans le répertoire unit-js ;
  • copier qunit.js et qunit.css dans le répertoire vendor ;
  • créer le fichier Javascript.test.html.

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>QUnit Example</title>
        <link rel="stylesheet" href="vendor/qunit.css">
    </head>
    <body>
    <div id="qunit"></div>
    <div id="qunit-fixture"></div>
    <script src="vendor/qunit.js"></script>
    <script src="Javascript.test.js"></script>
    </body>
    </html>
  • créer le fichier Javascript.test.js

    module('Javascript');
    
    test('should correctly return typeof null ', function () {
        equal(typeof null, 'object');
    });
    
    test('should correctly return typeof [] ', function () {
        equal(typeof [], 'object');
    });
    
    
    

Vous pouvez maintenant exécuter votre test en ouvrant le fichier Javascript.test.html dans un navigateur. L’url aura normalement la forme file://<chemin de votre application>/test/unit-js/Javascript.test.html 

Intégration du test Qunit dans Grails

Pour pouvoir exécuter ce test via Grails, nous allons avoir besoin d’exécuter du JavaScript coté serveur sur la JVM. Ici nous avons en plus besoin du DOM et donc d’une librairie "comprenant" le HTML. Nous allons dans notre exemple utiliser HTMLUnit.

Pour pouvoir exécuter les scripts JS, il faut créer le fichier _Events.groovy dans le répertoire script de grails avec le contenu suivant :

import com.gargoylesoftware.htmlunit.BrowserVersion
import com.gargoylesoftware.htmlunit.WebClient
import com.gargoylesoftware.htmlunit.html.HtmlPage
import org.codehaus.groovy.grails.test.GrailsTestTargetPattern
import org.codehaus.groovy.grails.test.GrailsTestType
import org.codehaus.groovy.grails.test.GrailsTestTypeResult
import org.codehaus.groovy.grails.test.event.GrailsTestEventPublisher
@Grab(group = 'net.sourceforge.htmlunit', module = 'htmlunit', version = '2.12')
class JsTestType implements GrailsTestType {

    @Override
    String getName() {
        'js'
    }
    @Override
    String getRelativeSourcePath() {
        //Rien à compiler pour le JS !
        null;
    }
    @Override
    int prepare(GrailsTestTargetPattern[] testTargetPatterns, File compiledClassesDir, Binding buildBinding) {
        //On ne peut pas savoir à l'avance combien il y aura de test,
        // donc on retourne un chiffre supérieur à 0 de façon à lancer l'execution des tests quand même
        1
    }
    @Override
    GrailsTestTypeResult run(GrailsTestEventPublisher eventPublisher) {
        def event = eventPublisher.event
        File jsUnitDir = new File("test/unit-js/")
        int failureCount = 0
        int successCount = 0
        def jsTestSuffix = ['test.html'] as String[]
        jsUnitDir.eachFileRecurse { file ->
            def fileName = file.getName()
            if (fileName.endsWith(".test.html")) {
                //On vérifie que le fichier ne matche pas chaque path. Si il y a au moins un cas ou ca ne match pas
                // => le fichier ne doit pas être traité
                def resultTest = runTest(file)
                failureCount += resultTest.bad
                successCount += resultTest.all - resultTest.bad
            }
        }
        new GrailsTestTypeResult() {
            @Override
            int getPassCount() {
                successCount
            }
            @Override
            int getFailCount() {
                failureCount
            }
        }
    }
    @Override
    void cleanup() {
    }

    private def runTest(File file) {
        WebClient client = new WebClient(BrowserVersion.FIREFOX_17)
        client.options.javaScriptEnabled = true
        HtmlPage page = client.getPage("file:///" + file.getAbsolutePath())
        def numberOfJobStillComputing = client.waitForBackgroundJavaScript(2000);
        if (numberOfJobStillComputing > 0) {
            return [
                    bad: 1,
                    all: 1
            ]
        }
        //Renvoit un objet contenant le nombre de test passant, le nombre de test en erreur et le nombre total de test
        def resultTest = page.executeJavaScript("QUnit.config.stats").javaScriptResult
        if (resultTest.bad > 0) {
            resultTest = [
                    bad: resultTest.bad,
                    all: resultTest.all
            ]
        }
        resultTest
    }
}

eventAllTestsStart = {
    if (getBinding().variables.containsKey("unitTests")) {
        // Ajoute le type Test à la phase unit
        unitTests << new JsTestType()
    }
}

 

Cette implémentation simpliste détecte correctement les erreurs, mais n’indique pas dans quel fichier. C’est améliorable, on va modifier la méthode runTest pour qu’elle renvoie des messages :

private def runTest(File file) {
        WebClient client = new WebClient(BrowserVersion.FIREFOX_17)
        client.options.javaScriptEnabled = true
        HtmlPage page = client.getPage("file:///" + file.getAbsolutePath())
        def numberOfJobStillComputing = client.waitForBackgroundJavaScript(2000);
        if (numberOfJobStillComputing > 0) {
            return [
                    bad: 1,
                    all: 1,
                    messages: ["There is still ${numberOfJobStillComputing} js job in background running after 2 seconds"]
            ]
        }
        //Renvoit un objet contenant le nombre de test passant, le nombre de test en erreur et le nombre total de test
        def resultTest = page.executeJavaScript("QUnit.config.stats").javaScriptResult
        if (resultTest.bad > 0) {
            def failedTests = page.getElementById("qunit-tests").querySelectorAll('li.fail')
            def messages = failedTests.collect { node ->
                if (node.querySelector('.test-name') != null) {
                    String moduleName = node.querySelector('.module-name')?.textContent?.toString()
                    String testName = node.querySelector('.test-name')?.textContent?.toString()
                    moduleName + ' - ' + testName
                } else {
                    null
                }
            }.findAll()
            resultTest = [
                    bad: resultTest.bad,
                    all: resultTest.all,
                    messages: messages
            ]
        }
        resultTest
    }

Et on va ajouter le traitement de ces messages dans la méthode run

GrailsTestTypeResult run(GrailsTestEventPublisher eventPublisher) {
        def event = eventPublisher.event
        File jsUnitDir = new File("test/unit-js/")
        int failureCount = 0
        int successCount = 0
        def jsTestSuffix = ['test.html'] as String[]
        jsUnitDir.eachFileRecurse { file ->
            def fileName = file.getName()
            if (fileName.endsWith(".test.html")) {
                //On vérifie que le fichier ne matche pas chaque path. Si il y a au moins un cas ou ca ne match pas
                // => le fichier ne doit pas être traité
                def resultTest = runTest(file)
                failureCount += resultTest.bad
                successCount += resultTest.all - resultTest.bad
                if (resultTest.bad > 0) {
                    event("StatusError", ["${resultTest.bad as Integer} tests on ${resultTest.all as Integer} failed in ${fileName}"])
                    def problems = resultTest.messages
                    problems.each { event("StatusError", [it]) }
                }
            }
        }
        new GrailsTestTypeResult() {
            @Override
            int getPassCount() {
                successCount
            }
            @Override
            int getFailCount() {
                failureCount
            }
        }
    }

Enfin, il est intéressant d’exploiter le mécanisme de pattern des tests grails, afin de ne lancer que certains tests. Par exemple, grails test-app :js J* ne lance que les tests commençant par J .

On va stocker le pattern comme attribut de classe :

GrailsTestTargetPattern[] testTargetPatterns
 
 @Override
 int prepare(GrailsTestTargetPattern[] testTargetPatterns, File compiledClassesDir, Binding buildBinding) {
        this.testTargetPatterns = testTargetPatterns
        //On ne peut pas savoir à l'avance combien il y aura de test,
        // donc on retourne un chiffre supérieur à 0 de façon à lancer l'execution des tests quand même
        1
 }

Et on va vérifier que le test respecte ce pattern :

@Override
GrailsTestTypeResult run(GrailsTestEventPublisher eventPublisher) {
        def event = eventPublisher.event
        File jsUnitDir = new File("test/unit-js/")
        int failureCount = 0
        int successCount = 0
        def jsTestSuffix = ['test.html'] as String[]
        jsUnitDir.eachFileRecurse { file ->
            def fileName = file.getName()
            if (fileName.endsWith(".test.html")) {
                //On vérifie que le fichier ne matche pas chaque path. Si il y a au moins un cas ou ca ne match pas
                // => le fichier ne doit pas être traité
                def isMatching = testTargetPatterns.collect { pattern ->
                    pattern.matchesClass(fileName, jsTestSuffix)
                }.find()
                if (isMatching) {
                    def resultTest = runTest(file)
                    failureCount += resultTest.bad
                    successCount += resultTest.all - resultTest.bad
                    if (resultTest.bad > 0) {
                        event("StatusError", ["${resultTest.bad as Integer} tests on ${resultTest.all as Integer} failed in ${fileName}"])
                        def problems = resultTest.messages
                        problems.each { event("StatusError", [it]) }
                    }
                }
            }
        }
        new GrailsTestTypeResult() {
            @Override
            int getPassCount() {
                successCount
            }
            @Override
            int getFailCount() {
                failureCount
            }
        }
}

Voilà, vous pouvez maintenant exécuter vos tests JavaScript commençant par la lettre J avec la commande : grails test-app :js J*

Conclusion

La solution ici présentée est une implémentation naïve de comment exécuter ses tests JavaScript avec Grails. Pour aller plus loin, il est possible par exemple d’ajouter la génération de rapport XML au format XUnit ou encore d’utiliser PhantomJs plutôt que HTMLUnit pour optimiser la vitesse d’exécution.

À l’heure où cet article est écrit, s’il n’y a pas de plugins pour Grails fournissant la fonctionnalité présentée ci-dessus, l’implementation proposée peut tout à fait servir de base pour la création d’un plugin adapté.

 

Benoît Lemoine
Développeur et fier de l'être, Benoit s'intéresse de près à tout ce qui peut permettre de créer une application web, du HTML aux sources de données, en passant par le javascript et les framework haute productivité. twitter : @benoit_lemoine

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *