Voici la retranscription de ma session “Chouchoutez votre code JavaScript“, présentée lors de la première édition de Devoxx France, le mercredi 18 avril 2012.

Le but de cette présentation est de montrer qu’il est aussi facile de tester et analyser son code JavaScript que son code Java. Les mêmes outils seront d’ailleurs utilisés, en particulier Maven, Jenkins et Sonar.

Avant toute chose, nous allons prendre underscore.js comme cobaye pour nos tests. Pour information, il s’agit d’une librairie proposant une soixantaine de fonctions JS permettant de faire de la programmation fonctionnelle.

Etape #1: Créer le projet

Mon choix, pour la première librairie de tests, c’est Jasmine. Il s’agit d’une librairie avec une communauté assez active, et disposant d’un certain nombre de supports : Java et Maven, .Net, Ruby, Node.js, etc.
Pour être plus précis, cette librairie va être surtout utilisée lorsque l’on souhaite faire du BDD (Behavior Driven Development). D’ailleurs, dans la terminologie de Jasmine, on ne parle pas de test, mais de spec.

Comme dit précédemment, Jasmine offre un support Maven. Pour ma présentation, j’opte pour la création d’un projet squelette, via un archetype Maven. Cela se fait via cette commande :

mvn archetype:generate
    -DarchetypeRepository=http://searls-maven-repository.googlecode.com/svn/trunk/snapshots
    -DarchetypeGroupId=com.github.searls
    -DarchetypeArtifactId=jasmine-archetype
    -DarchetypeVersion=1.1.0.1-SNAPSHOT
    -DgroupId=fr.devoxx
    -DartifactId=chouchoutez-javascript
    -Dversion=0.0.1-SNAPSHOT

Une fois ceci réalisé, nous obtenons un projet (que nous allons importer dans notre IDE préféré) ayant la structure suivante :

pom.xml
  ` src
     + main
     |  ` javascript
     |      ` quickstart.js
     ` test
        ` javascript
           ` quickstartTest.js

La partie intéressante dans le pom.xml est la déclaration du plugin Jasmine qui va permettre de lancer les tests via une commande Maven :

        <plugins>
            <plugin>
                <groupId>com.github.searls</groupId>
                <artifactId>jasmine-maven-plugin</artifactId>
                <version>1.0.1-beta-6</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>generateManualRunner</goal>
                            <goal>resources</goal>
                            <goal>testResources</goal>
                            <goal>test</goal>
                            <goal>preparePackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <jsSrcDir>src/main/javascript</jsSrcDir>
                    <jsTestSrcDir>src/test/javascript</jsTestSrcDir>
                </configuration>
            </plugin>

En lançant la commande mvn test, on voit que le test généré par défaut par l’archetype est lancé :

-------------------------------------------------------
 J A S M I N E   T E S T S
-------------------------------------------------------
[INFO]
describe The quickstart object
  it adds two numbers
[INFO]
Results:

1 specs, 0 failures

Etape #2 : Ecrire des tests

Je copie mon underscore.js dans le répertoire src/main/javascript, et nous allons le tester…
Jasmine propose une écriture assez intuitive des tests, assez proche de fest-assert. En voici un premier exemple :

describe("Mon premier test", function() {
    it("should be 42", function() {
        var resultat = _.max([1, 2, 3, 42]);
        expect(resultat).toBe(42);
    };
};

Avec Jasmine, on crée une suite de tests en utilisant le mot describe, suivi de son descriptif. Ensuite, on y ajoute une série de tests (voire on imbrique de nouvelles suites), chaque test (ou plus exactement chaque spec) étant défini par le mot clé it. On écrit le code du test, puis au moment de vérifier (i.e. de réaliser les assert), on va simplement utiliser expect suivi d’un matcher (ici toBe). Plusieurs autres matchers existent (toEqual, toBeNull, toBeGreaterThan, toContain, toBeUndefined).

A nouveau, je vérifie mes tests avec la commande Maven mvn clean test.

Voici mon fichier de tests final de la librairie underscore.js :

describe("Mon premier test", function() {

  it("should be 42", function() {
    var max = _.max([1, 2, 42]);
    expect(max).toBe(42);
  });

});

describe("Test d'underscore", function() {

    var myarray;

    beforeEach(function() {
        myarray = [1, 2, 3];
    });

    it("should return 42 when asked for last item", function() {
        var res = _.last([1,2,3,42]);
        expect(res).toBe(42);
    });

    it("be true", function() {
        var res = _.first([1, 2, 3]);
        expect(res == 1).toBeTruthy();
    });

    it("should be greater than 2", function() {
        expect(3).toBeGreaterThan(2);
    });

    it("should contains 2", function() {
        expect([1, 2, 3]).toContain(2);
    });

    it("should have size of 3", function() {
        expect(myarray.length).toBe(3);
    });

    it("should not be 1", function() {
        var res = _.max([1, 2]);
        expect(res).not.toBe(1);
    })

});

describe("test with spy", function() {

    it("should spy on last", function() {
        spyOn(_, 'last');
        var res = _.last([1, 2, 3]);
        expect(_.last).toHaveBeenCalled();
        expect(res).toBeUndefined();
    });

});

Vous remarquerez qu’à l’instar d’un @Before de JUnit, Jasmine offre la même possibilité avec beforeEach.
De plus, Jasmine offre aussi des fonctionnalités d’espionnage.

Etape #3 : Analyser son code

Pour analyser le code JavaScript, nous allons utiliser le merveilleux outil qu’est Sonar ! Il nous faut simplement y ajouter le plugin pour le support du langage JavaScript, qui se trouve ici.

Dans notre pom.xml, nous devons ajouter ces quelques lignes, pour spécifier que le projet est de langage JavaScript, et indiquer la localisation de nos sources (et celles des tests) :

    <properties>
        <sonar.language>js</sonar.language>
        <sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
    </properties>

    <build>
        <sourceDirectory>src/main/javascript</sourceDirectory>
        <testSourceDirectory>src/test/javascript</testSourceDirectory>

Il suffit désormais de lancer la commande mvn sonar:sonar pour réaliser l’analyse complète, puis de se rendre sur son serveur Sonar pour en lire les résultats !

Tout n’est pas encore parfait, car nous ne disposons d’aucune information propres aux tests : pourcentage de réussite, taux de couverture du code, etc.

Etape #4 : Analyse de la couverture

Le plugin JavaScript pour Sonar sait lire les résultats des tests exécutés par js-test-driver, une autre librairie de tests JavaScript, plus orientée TDD (Tests Driven Development, ou Développement piloté par les tests). Cette librairie apporte une syntaxe très proche de JUnit, en particulier concernant les assertions. En voici un exemple :

GreeterTest = TestCase("GreeterTest");

GreeterTest.prototype.testGreet = function() {
    var greeter = new myapp.Greeter();
    assertEquals("Hello World!", greeter.greet("World"));
};

Bon. Mais il faut tout réécrire, et perdre ce que l’on a fait avec Jasmine du coup ? Heureusement non ! Il existe en effet un adaptateur pour exécuter des tests Jasmine via js-test-driver.
Bien entendu, nous aurions pu partir dès le début avec la librairie js-test-driver, et oublier Jasmine, mais ça n’aurait pas été fun !

Donc pour faire marcher tout cela, il faut tout d’abord copier dans un répertoire lib/ les fichiers suivants :

  • jasmine.js (auparavant, il était chargé via le plugin Maven, que nous n’allons plus utiliser.
  • JasmineAdapter.js, le fameux adaptateur.

Puis, je crée un fichier jsTestDriver.conf à la racine du projet qui contient la configuration pour js-test-driver :

server: http://localhost:9876

load:
  - "lib/jasmine.js"
  - "lib/JasmineAdapter.js"
  - "src/main/javascript/*.js"
  - "src/test/javascript/*.js"

Ici c’est très simple, on indique le port où tournera le serveur js-test-driver, ainsi que les librairies à charger…

Dernière étape : modifier le pom.xml. D’abord, y supprimer les références au plugin Jasmine, devenu obsolète. Ensuite, ajouter ceci pour la dépendance vers le plugin Maven de js-test-driver :

    <dependencies>
        <dependency>
            <groupId>com.googlecode.jstd-maven-plugin</groupId>
            <artifactId>jstd-maven-plugin</artifactId>
            <version>1.3.2.5</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Puis je configure le plugin comme ceci :

    <build>
        ...
        <plugins>
            <plugin>
                <groupId>com.googlecode.jstd-maven-plugin</groupId>
                <artifactId>jstd-maven-plugin</artifactId>
                <configuration>
                    <port>9876</port>
                    <browser>/Applications/Firefox.app/Contents/MacOS/firefox-bin</browser>
                    <tests>all</tests>
                    <config>jsTestDriver.conf</config>
                    <testOutput>target/jstestdriver</testOutput>
                </configuration>
                <executions>
                    <execution>
                        <id>run-tests</id>
                        <goals>
                            <goal>test</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

On notera que certaines informations du fichier jsTestDriver.conf (comme le port du serveur) sont reprises ici. On spécifie aussi le chemin vers le navigateur utilisé par js-test-driver pour exécuter ses tests (chose inutile avec Jasmine, car ce dernier ne nécessite même pas de DOM pour exécuter les tests !).
Dernier point : j’ajoute le repository où se trouve le plugin js-test-driver :

  <repositories>
        <repository>
            <id>jstd-maven-plugin google code repo</id>
            <url>http://jstd-maven-plugin.googlecode.com/svn/maven2</url>
        </repository>
  </repositories>
  <pluginRepositories>
        <pluginRepository>
            <id>jstd-maven-plugin google code repo</id>
            <url>http://jstd-maven-plugin.googlecode.com/svn/maven2</url>
        </pluginRepository>
  </pluginRepositories>

Maintenant, pour voir si tout s’est bien passé, je vérifie que l’exécution des tests se réalise bien. J’exécute donc de nouveau la commande Maven mvn clean test :

-------------------------------------------
 J S  T E S T  D R I V E R
-------------------------------------------

Firefox: Runner reset.
..................
Total 18 tests (Passed: 18; Fails: 0; Errors: 0) (9,00 ms)
  Firefox 9.0.1 Mac OS: Run 18 tests (Passed: 18; Fails: 0; Errors 0) (9,00 ms)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Bravo ! C’est presque gagné ! Il ne nous reste plus qu’à pouvoir mesurer la couverture des tests. Rien de plus simple à ce stade : il suffit d’éditer le fichier jsTestDriver.conf et d’y ajouter les 4 lignes suivantes :

plugin:
  - name: "coverage"
    jar: "lib/coverage-1.3.4.b.jar"
    module: "com.google.jstestdriver.coverage.CoverageModule"

Je relance mvn clean package sonar:sonar, et l’analyse contient désormais les résultats des tests, mais aussi la couverture du code !

Etape #5 : Intégration continue

Lors de la présentation, j’ai manqué de temps et ai dû passé cette étape. Mais ici, rien de magique ni d’extraordinaire. En effet, comme depuis le début j’utilise des commandes Maven, les faire jouer par un serveur d’intégration continue est d’une simplicité enfantine. Je vous laisse donc le soin de jouer avec Jenkins ou tout autre serveur…

Conclusion

J’ai montré en une demi-heure environ qu’il était très simple de tester son code JavaScript, de l’analyser. Les mêmes outils sont d’ailleurs mis en oeuvre que pour le langage Java : Maven, Sonar et Jenkins.

Bref, il ne reste plus qu’à s’y mettre :)

Références

Start Slide Show with PicLens Lite PicLens