GWT & les tests, épisode 2
Dans le précédent article, nous avons démontré qu'il n'était pas si facile de faire des tests avec GWT car :
- La classe de test de base,
GWTTestCase
est trop restrictive (impossible d'utiliser des outils de tests), et est source de lenteurs - Le mock de composants GWT requiert l'utilisation d'interfaces intermédiaires plutôt que des classes de composants, ce qui induit un gros travail de refactoring sur les projets existants
Nous avons donc mis en place une solution alternative...
L'objectif
Pour faire efficacement des tests avec GWT, il nous semblait impératif :
- que nos classes de tests ne nécessitent pas de temps de chargement gênant
- de pouvoir manipuler les classes de composants GWT directement, sans interfaces intermédiaires qui rendent le projet plus complexe
- de pouvoir utiliser toutes les API Java, standards ou non, pas seulement celles émulées en JavaScript. Entre autres, il faudrait pouvoir utiliser les API d'introspection
java.lang.reflect
et des outils tels que uniutils - avoir quelque chose de léger, compatible avec Maven.
Dans le cadre d'un projet GWT en agile (ou nous avions donc besoin de faire des tests), nous avons développé une solution pour tester nos IHM. Nous avons fait en sorte de pouvoir instancier des widgets GWT sans lancer le HostedMode ou utiliser GWTTestCase.
Le framework "gwt-test-utils"
Nous avons mis au moins un un framework de test pour modifier les classes GWT de façon transparente pour le développeur. Nous pouvons donc utiliser les classes GWT dans une JVM standard. Cela passe par la modification "à chaud" du bytecode des classes de composants GWT pour remplacer les méthodes natives JSNI par des méthodes Java. Avec des images, voila ce que cela donne :
La présentation de l'implémentation technique de ce framework ne rentre pas dans le cadre de cet article. Nous nous concentrerons sur sa mise en œuvre.
Par ailleurs, nous avons donc commencé à mettre ce framework en open source : gwt-test-utils pour que tout le monde puisse l'utiliser.
La mise en œuvre
Commençons par écrire un simple test Junit 4 pour valider la création d'un bouton GWT :
@Test
public void checkText() {
Button b = new Button();
b.setText("toto");
Assert.assertEquals("toto", b.getText());
}
Comme nous l'avons expliqué dans le précédent article, un tel test tombe naturellement en erreur :
java.lang.ExceptionInInitializerError ...
Caused by: java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in
client code! It cannot be called, for example, from server code. If you are running a unit
test, check that your test case extends GWTTestCase and that GWT.create() is not called
from within an initializer or constructor.
at com.google.gwt.core.client.GWT.create(GWT.java:85)
at com.google.gwt.user.client.ui.UIObject.(UIObject.java:140)
... 23 more
Pour que le framework "gwt-test-utils" puisse réaliser les modifications de bytecode des classes GWT, il sera nécessaire d'exécuter nos tests avec un agent java que nous avons développé spécifiquement. Il faut donc rajouter l'argument VM dans la configuration d'exécution : -javaagent:chemin_vers_bootstrap.jar
.
Reste à initialiser "gwt-patch" dans le code de test :
@BeforeClass
public static void setUpClass() throws Exception {
//initialisation du framework de mock GWT
PatchGWT.init();
}
Le test peut maintenant être validé : "gwt-test-utlis" remplace à la volée le bytecode de la classe du composant. Ainsi, le HostedMode GWT n'est pas lancé en tâche de fond : le temps d'exécution est de l'ordre de quelques millisecondes. Et l'on peut utiliser tous les outils standards.
Par exemple, on peut utilise Easymock pour tester l'appel d'un service GWT-RPC :
static interface MyRemoteService extends RemoteService {
String myMethod(String param1);
}
static class MyGwtClass {
public String myValue;
public void run() {
MyRemoteServiceAsync service = GWT.create(MyRemoteService.class);
service.myMethod("myParamValue", new AsyncCallback<String>() {
public void onFailure(Throwable caught) {myValue = "error";}
public void onSuccess(String result) {myValue = result;}
});
}
}
@Mock
private MyRemoteServiceAsync mockedService;
@Test
public void checkGwtRpcOk() {
// Setup
// mock remote call
mockedService.myMethod(EasyMock.eq("myParamValue"), EasyMock.isA(AsyncCallback.class));
expectServiceAndCallbackOnSuccess("returnValue");
replay();
// Test
MyGwtClass gwtClass = new MyGwtClass();
gwtClass.myValue = "toto";
Assert.assertEquals("toto", gwtClass.myValue);
gwtClass.run();
// Assert
verify();
Assert.assertEquals("returnValue", gwtClass.myValue);
}
Note : l'annotation @Mock
est similaire à ce que l'on peut trouver avec Unitils. Elle sert à injecter un objet mocké.
Les contraintes et non contraintes
- Aucune contrainte sur la façon de concevoir / développer l'application GWT
- Modifier la commande de lancement des tests unitaires, en ajoutant l'option
-javaagent:chemin_vers_bootstrap.jar
. Il faut faire cela dans les "Run configurations" d'Eclipse, et dans la configuration Maven (configuration du plugin surefire de Maven). - Utiliser une JVM Java 6 pour exécuter les tests (une JVM 5 ne permet pas de modifier le code des méthodes natives). Cela est facile sous Eclipse, en changeant le JRE d'éxécution. Sous Maven, il suffit de changer la JVM utilisée par le plugin surefire.
Ces contraintes ne sont pas négligeables, mais sont supportables en comparaison du bénéfice apporté par les tests. Voir gwt-test-utils demo1 project pour un exemple complet de configuration avec Maven.
Les résultats
Sur notre projet (compilé avec JRockit 1.5, testé avec Hotspot 1.6) :
- 26k lignes dans l'application GWT
- 600 tests unitaires sur cette application, 14k lignes dans les tests
- 85% de couverture de tests sur l'application GWT !
Toutefois, nous avons concentrés nos tests sur la partie contrôleur de l'application GWT. Le but n'était pas de retester GWT, mais de valider le comportement que nous avons implémenté.
Conclusion
Le framework "gwt-test-utils" nous a permis de tester notre IHM de manière unitaire. Nous l'avons partagé dans un projet open source. Après cela, nous nous sommes aperçus que nous pouvions faire des tests bien plus intéressants : réaliser des tests d'intégration sur l'application complète (application GWT + partie serveur) dans une JVM standard, avec JUnit. Suite au prochain épisode ...