JBang et les tests unitaires

On ne présente plus le projet JBang et on a d’ailleurs vu ici comme démarrer un projet rapidement avec JBang.

JBang permet de commencer à écrire un programme Java très rapidement et ce sans se soucier de Maven, Gradle ni même d’IDE ; il est donc particulièrement recommandé pour les débutants en Java.

1. Un script JBang

Un script JBang n’est rien d’autre qu’une classe Java déclarée dans le package par défaut et qui a une visibilité soit publique soit défaut (package local).

Cette classe comporte:

  • une méthode main à la visibilité publique
  • des méthodes à la visibilité publique ou défaut
  • des méthodes privées (éventuellement)

2. Tester ce script JBang

Ce qu’on aimerait bien faire ici, c’est écrire une classe de test pour chaque script JBang, afin de garantir le bon fonctionnement de celui-ci et pourquoi pas écrire nos scripts JBang en faisant du TDD (Test Driven Development).

Pour cela, il faut s’intéresser aux méthodes que l’on va tester.

La méthode main retourne uniquement un code retour, va écrire sur la sortie standard, et peut-être interagir avec le système de fichiers: ce n’est probablement pas elle que l’on va tester.

Les méthodes Java de notre script prennent plusieurs paramètres en entrée et retournent un objet en sortie.

Elles sont publiques ou ont la visibilité défaut: elles sont donc accessibles depuis le package par défaut et il nous sera facile de tester ce qu’elles retournent en fonction des entrées qu’on va leur fournir.

On peut donc écrire un deuxième script JBang que l’on nommera par exemple ScriptTest.java, et qui sera le script de test de Script.java.

2.1. Note sur les méthodes privées

Vous remarquerez que si le script peut dans l’absolu faire usage de méthodes privées, cela n’apporte pas grand chose dans un script JBang.

En revanche, faire le choix de les rendre toutes publique ou avec la visibilité défaut présente l’avantage non négligeable de pouvoir toutes les tester !

3. Un script de test JBang

On va écrire un script JBang très simple qui affiche la liste des n premiers éléments de la suite de Fibonacci (en utilisant l’algorithme le plus simple possible, la performance n’étant pas notre problème ici):

$ cat Fibonacci.java 
///usr/bin/env jbang "$0" "$@" ; exit $?

//DEPS info.picocli:picocli:4.6.2

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

@Command(name = "Fibonacci", mixinStandardHelpOptions = true, version = "Fibonacci 0.1", description = "Fibonacci made with jbang")
public class Fibonacci implements Callable<Integer> {

    @Parameters(index = "0", description = "Number of elements to compute")
    private int max;

    public static void main(String... args) {
        int exitCode = new CommandLine(new Fibonacci()).execute(args);
        System.exit(exitCode);
    }

    @Override
    public Integer call() {
        List<Integer> suites = getFibonacciList(max);
        System.out.println(suites);
        return 0;
    }

    public List<Integer> getFibonacciList(int max) {
        List<Integer> suites = new ArrayList<>();
        for (int n = 0; n < max; n++) {
            suites.add(getFibonacci(n));
        }
        return suites;
    }

    public Integer getFibonacci(int n) {
        if (n == 0) {
            return 0;
        }
        if (n == 1) {
            return 1;
        }
        return getFibonacci(n - 1) + getFibonacci(n - 2);
    }
}

Et pour obtenir un script de test qui lui correspond, on va utiliser le template junit du catalogue JBang:

$ jbang init --template=junit@jbangdev Fibonacci.java 
[jbang] File initialized. You can now run it with 'jbang FibonacciTest.java' or edit it using 'jbang edit --open=[editor] FibonacciTest.java' where [editor] is your editor or IDE, e.g. 'netbeans'

Ce template permet de générer un script de test utilisant JUnit 5.

On obtient alors le script de test FibonacciTest.java:

$ cat FibonacciTest.java 
///usr/bin/env jbang "$0" "$@" ; exit $?

//DEPS org.junit.jupiter:junit-jupiter-api:5.8.2
//DEPS org.junit.jupiter:junit-jupiter-engine:5.8.2
//DEPS org.junit.platform:junit-platform-launcher:1.8.2

//SOURCES Fibonacci.java

import org.junit.jupiter.api.Test;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.LoggingListener;

import static java.lang.System.out;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;

// JUnit5 Test class for Fibonacci
public class FibonacciTest {

    // Define each Unit test here and run them separately in the IDE
    @Test
    public void testFibonacci() {
            assertEquals(1,2, "You should add some testing code for Fibonacci here!");
    }   

    // Run all Unit tests with JBang with ./FibonacciTest.java
    public static void main(final String... args) {
        final LauncherDiscoveryRequest request =
                LauncherDiscoveryRequestBuilder.request()
                        .selectors(selectClass(FibonacciTest.class))
                        .build();
        final Launcher launcher = LauncherFactory.create();
        final LoggingListener logListener = LoggingListener.forBiConsumer((t,m) -> {
            System.out.println(m.get());
            if(t!=null) {
                t.printStackTrace();
            };
        });
        final SummaryGeneratingListener execListener = new SummaryGeneratingListener();
        launcher.registerTestExecutionListeners(execListener, logListener);
        launcher.execute(request);
        execListener.getSummary().printTo(new java.io.PrintWriter(out));
    }
}

Vous noterez au début du script de test la ligne:

//SOURCES Fibonacci.java

qui indique que le script de test dépend du script de production correspondant.

On distingue ensuite deux parties dans ce script de test:

  • Un premier test testFibonacci qui échoue – c’est fait exprès afin que l’utilisateur le modifie et teste ce qui fait sens pour lui
  • Une méthode main qui lançera ce premier test ainsi que tous ceux que vous aurez ajouté par la suite et affichera ensuite un résumé de l’exécution des tests

La méhode main sera lançée par JBang à l’éxécution de FibonacciTest.java.

4. Edition du script JBang de test

Au delà d’écrire un script de test, ce qui nous intéresse ici est de pouvoir faire du TDD avec notre script JBang, ce qui nécessite de pouvoir éditer à la fois script de test et script de production.

C’est possible très facilement avec JBang:

$ jbang edit FibonacciTest.java 
[jbang] Running `sh -c idea /home/pyfourmond/.jbang/cache/projects/FibonacciTest.java_jbang_3d132c852ffb9462f5add1b68d3efa9e62dc6b3f0da7d2c1f60b0de69c5c5e22/FibonacciTest`
/home/pyfourmond/.jbang/cache/projects/FibonacciTest.java_jbang_3d132c852ffb9462f5add1b68d3efa9e62dc6b3f0da7d2c1f60b0de69c5c5e22/FibonacciTest

Comme j’ai défini la variable d’environnement suivante:

JBANG_EDITOR=idea

C’est IntelliJ IDEA qui se lance:

Et comme vous pouvez le voir, JBang nous permet d’utiliser IDEA pour éditer code de production ET code de test, et ce sans avoir à éditer le moindre fichier pom.xml ou build.gradle !

C’est la commande jbang edit qui a fait tout le travail pour nous !

Ici, j’ai ajouté 3 tests pour valider mon implémentation.

On peut évidemment lancer chacun de ces tests unitairement dans IDEA ainsi que l’ensemble des tests comme on a l’habitude de le faire avec IntelliJ IDEA.

5. Un répertoire de tests

JBang permet de faire l’économie de la sacro-sainte arborescence Java src/main/java et src/test/java.

Le répertoire src que l’on voit ici dans IntelliJ IDEA est généré par jbang edit et n’existe donc que dans le projet temporaire généré par cette dernière commande.

Tout cela pour dire que nos deux scripts Fibonacci.java et FibonacciTest.java sont situés à la racine du dépôt Git qui les contient.

Néanmoins, si notre dépôt Git contient plusieurs scripts JBang, on peut trouver cela plus propre de placer nos scripts de tests dans leur propre répertoire, par exemple tests.

On pourra alors procéder comme suit:

$ mkdir -p tests
$ jbang init -DscriptName=Fibonacci --template=junit5@grumpyf0x48 tests/FibonacciTest.java
[jbang] File initialized. You can now run it with 'jbang tests/FibonacciTest.java' or edit it using 'jbang edit --open=[editor] tests/FibonacciTest.java' where [editor] is your editor or IDE, e.g. 'idea'

Et il faudra ensuite modifier la directive SOURCES qui ne fait pas référence au bon chemin et qu’il faudra modifier en:

//SOURCES ../Fibonacci.java

Pour pouvoir lancer le test avec:

$ ./tests/FibonacciTest.java

Vous noterez qu’ici j’ai utilisé mon template JUnit de test JBang au lieu de celui du catalogue JBang pour lequel la commande aurait été:

$ jbang init --template=junit@jbangdev tests/Fibonacci.java

puisque dans ce cas c’est le template qui ajoute Test à la fin du nom du fichier.

6. Conclusion

On a vu dans ce billet que le fait d’écrire des scripts JBang ne nous dispensait pas d’écrire des tests unitaires !

L’utilisation d’un template pour les tests JUnit permet en une commande jbang de disposer d’un script de test qui s’éxécute.

Les tests peuvent être lancés depuis l’IDE comme on a l’habitude de le faire mais aussi en ligne de commande depuis le terminal.

Pour aller plus loin, on pourra exécuter les tests depuis un workflow GitHub, par exemple en utilisant la GitHub action de JBang, ou utiliser le nouveau plugin JBang pour IntelliJ IDEA.

Une autre piste intéressante à explorer serait de regrouper tous nos tests dans une « test suite ».

Note: Les sources utilisées dans ce billet sont disponibles.