Il y a 10 ans -
Temps de lecture 10 minutes
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 :
[groovy gutter= »true »]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()
}
}
[/groovy]
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 :
[groovy gutter= »true »]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
}[/groovy]
Et on va ajouter le traitement de ces messages dans la méthode run
[groovy gutter= »true »]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
}
}
}[/groovy]
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 :
[groovy gutter= »true »]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
}[/groovy]
Et on va vérifier que le test respecte ce pattern :
[groovy gutter= »true »]@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
}
}
}[/groovy]
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é.
Commentaire