Nous avions le besoin de rendre des vues codées en JSP pour tester du code JavaScript associé. Voici notre solution.

Notre problématique

Comme nous l’expliquions ici, nous avons beaucoup de JSP chez Hopwork, et du code JavaScript est en charge de “dynamiser” après coup certains composants, suivant une approche d’amélioration progressive. Dans ce cadre, comment écrire des tests simples et rapides pour ce code JavaScript, dans dupliquer le HTML présent dans les JSP, et sans devoir lancer un serveur ou utiliser Selenium ?

Étape 1 : utiliser jsdom

jsdom est un outil formidable : il implémente le Document Object Model (DOM pour les intimes) pour Node.js. Concrètement, il nous donne accès à un document HTML et aux APIs associées (ex : document.querySelector, element.classList), plus quelques API de base (window.location), presque comme un navigateur headless. A contrario, jsdom se moque des problématiques de rendu et donc de notre CSS.

Alors oui, son implémentation est incomplète, et parfois on ne pourra pas tester un truc un peu trop avancé qui utiliserait une fonctionnalité encore manquante. Mais dans 95% des cas ce sera tout bon, et pour les 5% restant il est en général possible de mocker la fonctionnalité manquante, ou d’écrire son code un poil différemment pour que le test puisse l’ignorer. Dans le pire des cas, rien n’empêche d’ajouter un petit test Selenium.
Personnellement, je ne jure que par jsdom depuis 2 ans pour mes tests de code front-end.

Voici à quoi ressemble un de nos tests utilisant jsdom :

Nous avons masqué le code boilerplate créant l’environnement jsdom derrière la fonction décoratrice withDocument, qui nous rend quelques autres services :

  • Elle prend optionnellement un premier paramètre de configuration pour définir par exemple le code HTML de la page ou sont fichier source, ou encore des modules JavaScript à charger dans la page.
  • Les objets les plus utiles à notre code front sont passés en arguments : jQuery ($), window (win), et document (doc). (window et document sont par ailleurs accessible de manière globale, mais cela met en avant le fait que ces variables ne sont accessibles qu’au sein de l’environnement jsdom, et cela permet de les aliaser facilement si désiré).
  • Aide aux tests asynchrones : si la fonction de test déclare un 4ème argument, alors une fonction “callback” lui est donnée pour cet argument, à appeler pour signaler la fin du test. Alternativement, la fonction de test peut également retourner une promesse.

Le code complet de notre setup de test est disponible dans ce gist.

Étape 2 : interpréter des JSP en JavaScript

Nous avons donc un outil sympa pour écrire des tests avec un document HTML, mais il reste un problème de taille : comment éviter de dupliquer le code HTML contenu dans nos JSP ?

Je vais passer ici le récit à la première personne, afin qu’il soit clair que je suis le seul fautif pour ce qui a été fait ensuite 🙂

Cette histoire de JSP me gênait franchement, j’ai donc réfléchi à quelques solutions possibles. Tout d’abord j’ai cherché un interpréteur existant sur Internet, sans grand espoir, et sans résultat. Puis j’ai envisagé d’appeler l’interpréteur de JSP de Tomcat (Jasper) depuis notre code JavaScript, mais le fonctionnement même des JSP rend l’opération tout sauf évidente. Restait la dernière idée : interpréter des JSPs en JavaScript, littéralement.

Waaatt!?

Et puis après tout, nous n’avions pas besoin de tout supporter : seulement les variables (${foo}), les structures de contrôle de base (c:if, c:forEach, etc.), quelques utilitaires en plus que nous utilisons (quelques tags fournis par Spring), et les inclusions d’autres JSP évidemment. Par contre, je suis fainéant et l’idée d’écrire un parseur ne m’enchantait pas spécialement, et ne rentrait pas du tout dans la case “petit hack, gros impact”.
Bref, long story short, après 3-4h de coding en TDD j’avais un transformateur de JSP en templates lodash, tout ça fait à coup de RegExp. Voilà, voilà.
Par exemple : <c:if test="${someConfition}"> est transformé en <% if (someCondition) { %>.

Après avoir écrit quelques tests avec, l’outil a fait la preuve de son utilité. Alors nous l’avons gardé, parce que ça juste marche. Et depuis nous sommes repassé très peu dessus, juste 1 à 2 heures tous les quelques mois, afin d’ajouter un petit truc qui manquait. Mais ça n’est jamais devenu un gouffre temporel.

Du coup, voici un exemple (simpliste) de test écrit avec notre interpréteur transformateur. Le code JS testé est omis, il n’apporterait rien ici.

La JSP :

Le test :

Oui, ça ressemble beaucoup à l’exemple précédent, et c’est bien là le but. On dit au test où trouver la JSP à charger, et on lui donne les attributs nécessaires (uniquement param.path dans le cas présent). Et c’est tout. Aussi, si la JSP en inclut d’autres, elles seront résolues également.

Si la description de la solution ne vous a pas fait partir en courant et que vous voulez en voir plus, vous pouvez en lire le code du transformateur de JSP dans cet autre gist.

Ce que nous y avons gagné

Nous pouvons désormais facilement écrire des tests JS ciblant un unique composant graphique, tout en utilisant des JSP. Plus d’excuses donc pour ne pas tester ces composants, voire pour ne pas les développer en TDD. D’un point de vue temps d’exécution, nos tests sont plutôt rapides, considérant qu’ils tournent avec un vrai DOM. Pour preuve, voici la sortie de nos tests les plus lents :

 Editable Tags
 ✓ should be enhanced on init (105ms)
 ✓ should build tags from input value (83ms)
 ✓ should add entered tag to list when hitting COMMA or ENTER (115ms)
 ✓ should add entered tag to list when quitting field (68ms)
 ✓ should remove last tag when hitting BACKSPACE while not editing a new tag (79ms)
 ✓ should not remove last tag when hitting BACKSPACE while editing a new tag (60ms)
 ✓ should lose focus on TAB (57ms)
 ✓ should not submit form on ENTER (62ms)
 ✓ should focus input when user clicks in container (60ms)
 ✓ should not focus input when user clicks on tag (62ms)
 ✓ should expand input and show placeholder when no tags are entered (85ms)
 - should add focused style to container when input is focused
 ✓ should feed underlying input at all times (81ms)
 ✓ should switch between enhanced and basic mode when fallback is enabled (81ms)
 ✓ should remove specific tag on user request (66ms)
 ✓ should visually render less important tags as such (145ms)
 ✓ should reject duplicates (72ms)
 ✓ should trigger change event on underlying input (107ms)
 ✓ should udpdate the placeholder when the profile family changes (112ms)

Leave a Reply

Your email address will not be published. Required fields are marked *