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 :

gwt

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 :

1
2
3
4
5
6
7
@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 :

1
2
3
4
5
6
7
8
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 :

1
2
3
4
5
@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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 …

Mots-clés: , ,

4 commentaires pour “GWT & les tests, épisode 2”

  1. Bonjour,

    Est-ce que le premier test indiqué en haut du post fonctionne? Car même si les appels JSNI seront transparents le bouton fait appel au navigateur qui n’est pas présent.

    Quelle est la différence entre votre mécanisme et l’utilisation de :

    GWTMockUtilities.disarm()

    Merci,

  2. Bonjour,

    Justement, le principal avantage de notre solution par rapport à GWTMockUtilites est que notre solution opensource permet d’instancier et d’utiliser les composants GWT dans une JVM (et rien que dans une JVM), sans passer par un mock.

    GWTMockUtilites permet effectivement d’écrire des tests unitaires complexes sur les IHM, mais nécessite d’architecturer son code pour avoir un contrôleur séparé de l’IHM et qui se charge de la mettre à jour.
    Dans ce cas, seul le contrôleur est testé unitairement puisque la vue est mockée.

    Notre mécanisme permet également de tester les IHM avec des EventHandler qui mettent à jour directement des autres champs, sans passer par un contrôleur intermédiaire.
    De plus, on s’affranchit de la déclaration des mocks qui peuvent réduire la lisibilité des tests unitaires.

  3. Bonjour,
    votre framework m’intéresse, et je voudrais le tester. sauf que :
    Quand je rejoute le javaagent, j’ai l’erreur suivante :

    Error occurred during initialization of VM
    agent library failed to init: instrument
    Error opening zip file or JAR manifest missing :

    Merci de votre aide.

  4. Bonjour,
    Depuis la sortie de la version 2 de GWT, nous avons modifié le framework gwt-test-utils pour se passer du javaagent.
    Aujourd’hui, la modification des classes de GWT se fait au moment du chargement des classes par un classloader custom.

    La documentation du framework a été mise à jour en conséquence, je vous invite vivement à la consulter :

    http://code.google.com/p/gwt-test-utils/wiki/UserGuide

    Et si vous avez toujours des questions, je vous invite à passer par la mailing list (en anglais) : gwt-test-utils-users@googlegroups.com

Laissez un commentaire