GWT & les tests, épisode 1

le 06/10/2009 par Gael Lazzari
Tags: Software Engineering

GWT est un framework permettant de créer une interface Web riche en Java plutôt qu'en HTML et Javascript. La programmation de l'interface ressemble beaucoup à du Swing : new Panel(), new Button(), add ClickListener... C'est une approche assez séduisante : pas besoin de connaître un nouveau langage, possibilité de réutiliser les outils que l'on utilise en Java... De plus, comme tout est en Java (même la partie vue du modèle MVC!), on devrait donc pouvoir faire des tests sur l'IHM. Essayons donc.

Première tentative (en TDD :-) )

Prenons un exemple simple : on veut afficher un champ de saisie texte, un bouton et un label. Quand on clique sur le bouton, le contenu du champ texte saisi par l'utilisateur est placé dans le label. Écrivons d'abord le test correspondant (en JUnit 4) :

@Test
public void testClick() {
	GwtExample view = new GwtExample();
	Assert.assertNull(view.getLabel().getText());
	view.getTextBox().setText("mon text");
	// création d'un évènement "clic" basique
	NativeEvent event = Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false);
	// dispatch de l'évènement
	DomEvent.fireNativeEvent(event, view.getButton());
	Assert.assertEquals("mon text", view.getLabel().getText());
}

A l'aide de la fonctionnalité "quick fix eclipse" (et d'un peu de connaissance de GWT :-)), on écrit le code de la vue correspondant :

public class GwtExample extends Composite {
	private Label label;
	private TextBox textBox;
	private Button button;

	public GwtExample() {
		FlowPanel fp = new FlowPanel();
		textBox = new TextBox();
		fp.add(textBox);
		button = new Button("valider");
		fp.add(button);
		button.addClickHandler(new ClickHandler() {

			public void onClick(ClickEvent event) {
				label.setText(textBox.getText());

			}
		});
		label = new Label("init");
		fp.add(label);
		initWidget(fp);
	}

	public Label getLabel() { return label; }
	public TextBox getTextBox() { return textBox; }
	public Button getButton() { return button; }
}

Lançons le test JUnit précédent :

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

Qu'est ce que cela signifie ? Que les classes GWT ne sont pas destinées à être utilisées dans une JVM standard, mais fonctionnent seulement une fois compilées en Javascript, et exécutées dans un navigateur. Aïe. Adieu les tests ?

Des solutions existantes

GWTTestCase

GWT fournit une classe pour faire des tests unitaires : GWTTestCase. Cette classe présente un certain nombre de défauts :

  • La gestion des évènements natifs n'est implémentée que depuis GWT 1.6. GWTTestCase ne permet pas de tester la partie Vue de manière efficace sur les versions précédentes.
  • Lenteur. En fait cette classe lance un HostedMode masqué, il lui faut donc plusieurs secondes pour s'initialiser. Cela rend difficile le TDD, même en utilisant GWTTestSuite pour ne lancer qu'un HostedMode par module GWT testé.
  • Localisation des fichiers. Les tests unitaires doivent être compilés par GWT. Ils doivent donc être référencés par le fichier MonApp.gwt.xml. Cela complique pas mal le lancement et le packaging de l'application, surtout avec Maven.

De plus, l'utilisation de GWTTestCase et de GWTTestSuite limite fortement l'accès aux APIs Java : les tests unitaires doivent être compatibles avec le compilateur GWT, qui se charge de compiler les IHM Java en Javascript. Il n'est alors pas possible d'utiliser des librairies de tests telles que Unitils ou Easymock. La liste des classes Java émulées en JavaScript est disponible ici.

Utilisation d'interfaces

Une solution pour tester GWT est de remplacer tous les objets GWT par des objets mock : des classes qui simulent le comportement des classes GWT, mais qui fonctionnent dans une JVM standard. Cette solution est possible, mais elle a un gros inconvénient : comme les composants GWT ne sont pas des interfaces mais des classes concrètes, il va donc falloir modifier le code de notre application pour qu'elle utilise des interfaces au lieu de ces classes, pour que l'on puisse mocker dans nos tests, comme ici. Cela nous oblige donc à modifier lourdement le code de notre application, et à écrire des mocks pour toutes les classes GWT que l'on va utiliser. On doit pouvoir faire mieux.

Le noeud du problème

Afin d'avancer, nous avons donc regardé pourquoi Google avait bloqué le fonctionnement des classes GWT dans une JVM standard. La raison est simple : une bonne partie du code de ces classes est du code JSNI. Ce code se présente de la manière suivante :

public static native void alert(String msg) /*-{   $wnd.alert(msg); }-*/;

C'est une fonction native. C'est à dire que lors de l'exécution, la JVM va essayer d'exécuter une fonction ayant le même nom dans une DLL (ou un .so). Quand ces classes sont compilées en Javascript, la méthode est remplacée par le code situé entre /* et */. C'est pourquoi on ne peut les exécuter dans une JVM standard, non Javascript. Voir ici pour plus de détails sur JSNI.

Une autre conséquence est qu'une partie du fonctionnement de GWT est implémenté, non pas en Java, mais en Javascript. Même si on arrive à contourner le problème des méthodes natives, il faudra trouver une solution pour maintenir ce fonctionnement.

Conclusion

Tout cela n'est guère encourageant. N'y a t il donc pas moyen de faire des tests sur notre belle IHM écrite en Java ? La suite au prochain épisode ...