Pourquoi tester son code javascript ?
Lorsque l'on cherche à faire du TDD, on apprécie fortement de pouvoir tester tout son code. Ne pas pouvoir tester certaines parties parce qu'elles sont développées dans un langage différent est particulièrement frustrant. Et surtout on perds tout l'intérêt d'écrire des tests car il faut tester manuellement une partie de l'application.Dans le cas d'une application web, c'est notamment le problème avec les tests javascripts. Depuis plusieurs mois, je cherche une solution à ce problème. J'ai même fait un atelier à ce sujet lors de Paris Web 2010. Tester ses applications javascript avec QUnit et Selenium.
Divers problèmes m'ont cependant fait m'éloigner de QUnit. Vous comprendrez plus bas.
La problématique
Nous avons une application rails (mais cela pourrait très bien être une application django ou utilisant toute autre technologie serveur). Cette application a de nombreuses fonctionnalités et manipule les données côté serveur.
Pour diverses raisons, nous souhaitons exporter une partie de la manipulation des données (tout ce qui est de l'affichage) du côté du navigateur. Ainsi, nous économisons notre architecture serveurs en utilisant le CPU de l'utilisateur. Notre application va donc recevoir des données depuis le serveur (l'application rails) au format JSON, les traiter et les afficher.
Du côté de notre application rails, nous écrivons tout plein de tests en utilisant Test::Unit ou RSpec.
Mais nous désirons également tester notre javascript.
Diverses solutions
Plusieurs solutions existent afin de tester ce javascript. Faisons-en un petit récapitulatif.
Selenium
Selenium exécute un ou plusieurs navigateurs, charge la page et permet de faire des requêtes XPath sur celle-ci. Très puissant, il peut par exemple vous permettre de vérifier la présence d'un lien dans une page. Ou encore de faire un screenshot de cette même page.
L'avantage étant que vu que c'est directement le navigateur qui est exécuté, tous vos javascripts le sont également.
Deux inconvénients sont à déplorer :
- Sélenium implique d'avoir le serveur du même nom de lancé. C'est du java et c'est assez lourd.
- Les seules requêtes que vous pouvez faire avec Selenium sont des requêtes XPath. Si vous n'en maitrisez pas correctement la syntaxe, cela peut se révéler problématique.
QUnit/JSpec
QUnit est le framework de test javascript de jQuery. JSpec est fortement inspiré de RSpec.
Il s'agit de deux frameworks de test en javascript. Vous écrivez vos tests dans ce langage, ils exécutent votre application et vous signalent si vos tests passent ou non. Tester son code javascript avec QUnit est en soi assez simple.
Dans la pratique, c'est un petit peu plus problématique. En effet, pour exécuter vos tests, vous devez charger une page. Vous devez donc charger autant de pages qu'il y en a dans votre application. Vu que votre code javascript doit être exécuté, vous ne pouvez pas vous contenter de faire un curl sur chacune de ces pages et d'en vérifier le contenu. Il faut donc coupler Selenium à votre framework de tests afin de pouvoir exécuter ceux-ci. Nous en revenons à la problématique précédente avec la dépendance à un serveur java assez lourd.
Johnson / Harmony / Holygrail
Et il y a deux mois, miracle ! Je suis tombé sur johnson. Désolé pour les développeurs non ruby, ça devient spécifique au plus beau langage informatique la ;)
Cette librarie écrite en ruby utilise SpiderMonkey afin d'exécuter du code javascript. Vous pouvez donc faire :
Johnson.evaluate("4 + 4")
A johnson, nous ajoutons une couche pour le DOM : harmony. Cette librairie utilise johnson pour exécuter le javascript. A cela elle ajoute la gestion du DOM HTML. elle vous permet de charger une page HTML ainsi que tous ses javascripts ... et de l'exécuter ! Ainsi vous pouvez même charger jQuery ou toute autre librairie et utiliser ses fonctionnalités dans vos tests !
Exemple :
page = Harmony::Page.new(<<-HTML)
<html>
<head>
<title>Hello World !</title>
</head>
<body></body>
</html>
HTML</p>
<p>page.execute_js("document.title") #=> "Hello World !"
<html>
<head>
<title>Hello World !</title>
</head>
<body></body>
</html>
HTML</p>
<p>page.execute_js("document.title") #=> "Hello World !"
Cela commence à devenir intéressant ! :)
Enfin nous ajoutons une dernière couche car nous sommes dans une application rails : holygrail. Cette librairie utilise harmony et permet d'exécuter du javascript sur le contenu rendu par votre test ruby, que ce soit Test::Unit ou RSpec.
Tests unitaires
Voici un exemple de test avec RSpec :
require 'spec_helper'</p>
<p>describe MyController do
integrate_views
include ActionController::Assertions::HolyGrail
before(:each) do
login_as users(:first)
get :index
response.should be_success
end
it 'should return a missing i18n string' do
js("I18n.t('test');").should eql('test')
end
it 'should return an i18n string' do
js("I18n.t('save')").should eql('Sauvegarder')
end
end
Ici, nous testons une librairie de traductions de chaines de caractères en javascript. Nous récupérons le contenu de notre page afin d'avoir toutes nos librairies javascript. Et en conservant ce contexte, nous vérifions que telle ou telle fonction retourne la valeur appropriée.
Nous pouvons donc déjà maintenant tester correctement notre javascript. L'avantage qui a, dans mon cas, permis de faire ce choix, c'est que les tests sont écrits en Ruby et sont donc présent avec tous vos autres tests. Vous ne faites plus de différenciation entre les tests de votre code ruby et ceux du code javascript. Et il n'y a pas deux méthodes différentes pour exécuter ces tests. Lorsque vous lancez ceux-ci, tous sont lancés.
Tests ajax
Seule, cette solution a cependant une limite assez énorme : vous ne pouvez pas faire d'appels ajax. En effet, lorsque vous exécutez vos tests, il n'y a aucun serveur web de lancé. Vos appels ajax vont donc échouer lamentablement en tapant dans des erreurs dns.
Dans la pratique, c'est un petit peu différent car harmony est intelligent et redirige lui même tous vos appels ajax. Si vous appeller /test, ce n'est pas sur http://test.host/test que la requête sera faite. Mais sur file:///test. Cela a cependant ses limites. Allez vous amuser à mettre toutes vos fixtures de test à la base de votre système rien que pour les beaux yeux de votre application ;)
Moqueur
Ne trouvant pas de solution à ce niveau, j'ai donc décidé de créer la mienne ! Et cela donne moqueur. Son nom est un chouilla équivoque. Moqueur permet de mocker vos appels ajax jQuery.
Reprenons notre url /test de tout à l'heure. Vous faites un appel ajax sur cette url :
jQuery.ajax({url: '/test'});
La valeur que doit vous retourner cet appel ajax est une string "Hello World". Mockons donc cette requête.
jQuery.mockAjax({
url: '/test',
content: "Hello World !"
});
Badoum ! Plus aucune requête ajax ne sera faite sur cette URL. Le contenu retourné sera toujours "Hello World !".
Intégration à Rails
La problématique est maintenant : ou, quand et comment j'intègre cela dans mon application rails ? En effet vous ne voulez pas mocker vos appels ajax pour vos utilisateurs.
Dans mon application, nous utilisons un dérivé de la stratégie de bundle javascript de github. En conséquent en faisant, dans ma vue :
<%= javascript_bundle "jquery" %>
Il suffit donc simplement de vérifier que l'on est bien dans l'environnement de test. Et si c'est le cas, j'inclue tous mes javascripts de test : moqueur et tous les mocks qui vont avec.
<%= javascript_bundle 'test' if Rails.env.test? %>
Zou, y'a plus qu'à écrire des tests ! :)
Evolutions de moqueur
A l'heure actuelle, moqueur est assez primitif. Il réponds toujours la même chose pour une même requête quelque soit la méthode utilisée, le type demandée et les paramètres transmis. Plusieurs évolutions sont cependant prévues et arriveront quand j'aurai le temps de les implémenter :
- Mocker une requête en fonction de son type : GET, POST, PUT, DELETE
- Mocker une requête en fonction des paramètres transmis (en GET ou POST)
- Permettre à un mock de rendre une requête échouée
Ainsi que, potentiellement, les diverses améliorations que vous trouveriez utiles. N'hésitez pas à ouvrir un ticket à ce propos.
Conclusion
J'ai cherché pendant assez longtemps une solution viable permettant de tester convenablement mon code javascript. J'en parlais d'ailleurs avec Jean Michel lors du dernier apéro Ruby à Lyon. Et la conclusion que nous avions eu était que à l'heure actuelle, c'était trop prise de tête.
Je ne pensais pas trouver une solution qui me convienne moins de deux mois plus tard ! :) Et vous, vous testez votre code javascript ? Comment ?


Commentaires
salut
si je comprends bien, on se passe du browser pour tester JS car celui ci est interprété côté serveur ? du coup on prend le risque de passer à côté de bugs spécifiques aux browsers non ? est ce qu'avec cette solution JS peut modifier le DOM, et celui est testable par la suite ? quid des events type mouseover ou onsubmit après avoir appuyé sur entrée, sont ils émulables ?
Effectivement on se passe du browser. Johnson utilise le moteur de Firefox. Donc si c'est un bug spécifique à Firefox, on tombera dessus. Pour les bugs spécifiques à IE et Chrome, on ne tombera pas dessus.
Il serait en revanche tout à fait envisageable d'exécuter le code avec V8 pour la cross compatibilité avec Chrome. Pour tester avec IE, il faudra se contenter de Selenium par contre.
C'est interprété en ruby. Pas réellement "côté serveur" car les tests étant interprétés en local, il n'y a pas de "serveur". Uniquement la machine qui exécute ces tests ;)
Merci pour le commentaire du "PS". Je vais bientôt remplacer Wordpress par une application maison. Je rajoute la notification par email des commentaires dans ma todolist ;)
Bonjour, Dans la partie sur "QUnit/JSpec", vous recommandez de coupler les tests unitaires avec Selenium. Pour ma part, j'utilise JSTestDriver qui permet d'exécuter toutes les suites de tests unitaires définie en JavaScript. Cela présente l'avantage de pouvoir industrialiser le développement JavaScript en automatisant l'exécution des tests quotidiennement.
Voila, cétait mes 2 cents...
PS : J'aime bien le site et le contenu (dans le Reader) mais il manque un bouton pour recevoir les nouveaux commentaires...