GWT is a framework developed by Google for building AJAX enabled web applications using the Java programming language. It comprises:
- An API for creating GUI (similar to Swing), which manipulates the web browser's Document Object Model (DOM).
- A Java-to-JavaScript compiler.
- An environment for running and debugging GWT applications.
This approach offers some advantages:
- Assuming you know Java, there is no need to learn a new programming language.
- You can build an AJAX enabled web application without writing JavaScript or HTML.
- Everything is written in Java, so Java's advanced development tools, including debugging and re-factoring support, are available.
- GWT shields the developer from browser idiosyncrasies
Finally, since everything is in Java (even the View part of the MVC pattern), we should be able to create UI tests easily. This article explores some approaches.
1. First attempt
1.1 GWT using TDD
We will start with a simple example: we want to display a text entry field, a button and a label. When we click on the button, the text field content entered by the user is placed in the label. The corresponding test (in JUnit 4) will be:
1 @Test 2 public void testClick() { 3 GwtExample view = new GwtExample(); 4 Assert.assertNull(view.getLabel().getText()); 5 view.getTextBox().setText("my text"); 6 // creation of a basic "click" event 7 NativeEvent event = Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false); // dispatch de l'évènement DomEvent.fireNativeEvent(event, view.getButton()); 8 Assert.assertEquals("my text", view.getLabel().getText()); 9 }
Then, we can write the corresponding View code:
1 public class GwtExample extends Composite { 2 private Label label; 3 private TextBox textBox; 4 private Button button; 5 6 public GwtExample() { 7 FlowPanel fp = new FlowPanel(); 8 textBox = new TextBox(); 9 fp.add(textBox); 10 button = new Button("validate"); 11 fp.add(button); 12 button.addClickHandler(new ClickHandler() { 13 14 public void onClick(ClickEvent event) { 15 label.setText(textBox.getText()); 16 17 } 18 }); 19 label = new Label("init"); 20 fp.add(label); 21 initWidget(fp); 22 } 23 24 public Label getLabel() { return label; } 25 public TextBox getTextBox() { return textBox; } 26 public Button getButton() { return button; } 27 }
Finally, we launch the preceding JUnit test:
1 java.lang.ExceptionInInitializerError ... 2 Caused by: java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in 3 client code! It cannot be called, for example, from server code. If you are running a unit 4 test, check that your test case extends GWTTestCase and that GWT.create() is not called 5 from within an initializer or constructor. 6 at com.google.gwt.core.client.GWT.create(GWT.java:85) 7 at com.google.gwt.user.client.ui.UIObject.(UIObject.java:140) 8 ... 23 more
The above error means that GWT classes are not to be used in a standard JVM; they only work once they are compiled into JavaScript, and executed in a browser.
1.2 Some existing solutions
1.2.1 GWTTestCase
GWT provides a class to perform unit tests called GWTTestCase, however it suffers from a number of limitations: I
- The management of native events is only implemented from GWT 1.6. GWTTestCase does not allow testing the View part in an efficient manner as per the previous versions.
- Slowness. In fact, this class launches a masked HostedMode (the development environment for GWT), which means it needs a few seconds to initialize itself.
- Locating files. The unit tests have to be compiled by GWT. They must therefore be referenced by the MyApp.gwt.xml file. This complicates the application launch and packaging quite a bit, especially with Maven.
Moreover, using the GWTTestCase and GWTTestSuite greatly limits access to Java APIs: the unit tests must be compatible with the GWT compiler (which takes care of compiling the Java UI code into JavaScript). This means that, for example, using Java reflection is not possible. It is therefore not possible to use test libraries such as Unitils or Easymock.
The list of Java classes emulated in JavaScript is available here.
GWT 2.0 brings an improvement to GWTTestCase in that the class no longer uses native libraries to run tests. HtmlUnit is used instead of the browser used by Hosted Mode. In GWT 2, GWTTestCase is platform independent. But some limitations of GWTTestCase are still there: test execution is slow and there is no possibility to use a standard test framework.
1.2.2 Using interfaces
A solution to test a GWT application is to not use GWT objects while testing but instead replace all GWT objects with mock objects that will work in a standard JVM. This solution presents a major inconvenience however. Since the GWT objects are not interfaces but rather concrete classes, we will have to modify the code of our application so that it uses interfaces (you can find an example here). This solution impacts the design of our application.
1.3 The heart of the problem
In order to move forward, we have investigated why Google blocked the execution of GWT classes in a standard JVM. The reason is simple: a good portion of the code for these classes uses JSNI code. JSNI code is presented in the following manner:
1 public static native void alert(String msg) /*-{ $wnd.alert(msg); }-*/;
This is a native function. It means that during the execution, the JVM will try to execute a function that has the same name as a DLL (or a .so). When these classes are compiled into JavaScript, the method is replaced by the code located within the /* and */. It’s the reason we cannot execute within a standard JVM, using non-JavaScript.
In addition, part of the GWT behaviour is not implemented in Java, but in JavaScript (or by the brower’s HTML rendering system). Even if we succeed in circumventing the problem of the native methods, we have to find a solution to reimplement this behaviour.
2. Gwt-Test-Utils framework
2.1 Objective
Our objectives to be efficient in GWT testing are as follow:
- Our test classes should not require any annoying loading time.
- We should be able to manipulate the GWT classes directly, without intermediary interfaces that render the project more complex.
- We should be able to use all the Java Standard APIs, specifically the introspection Java APIs (to use tools like Unitils).
- We want something that is light and compatible with Maven.
2.2 The "gwt-test-utils" framework
During a customer project, we developed a framework which answered to our objectives.
We have designed a test framework to modify the GWT classes without any additional work for the developer. It starts with "hot" modifications of the bytecode of the classes for the GWT objects, to replace the native JSNI methods by Java methods, as shown in the following diagram:
Note: the presentation of the technical implementation of the framework is not part of the scope for this article. We will concentrate on its usage.
We have published this framework as an open source project, Gwt-Test-Utils, so that anyone can use it.
2.3 Using the framework
We will start by writing a simple Junit 4 test to validate the creation of a GWT button:
@Test public void checkText() { Button b = new Button(); b.setText("toto"); Assert.assertEquals("toto", b.getText()); }
As we explained earlier, such a test generates an error:
1 java.lang.ExceptionInInitializerError ... 2 Caused by: java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in 3 client code! It cannot be called, for example, from server code. If you are running a unit 4 test, check that your test case extends GWTTestCase and that GWT.create() is not called 5 from within an initializer or constructor. 6 at com.google.gwt.core.client.GWT.create(GWT.java:85) 7 at com.google.gwt.user.client.ui.UIObject.(UIObject.java:140) 8 ... 23 more
For Gwt-Test-Utils to be able to modify GWT classes’ bytecode, it will be necessary to execute our tests with a Java agent that we have specifically developed. We must therefore add a new argument to the JVM launching command: -javaagent:path_to_bootstrap.jar.
After that, we have to install " Gwt-Test-Utils " within the test code:
1 @Before 2 public static void setUpClass() throws Exception { 3 // patch GWT standard components 4 PatchGWT.init(); 5 } 6
The test can now be validated: Gwt-Test-Utils replaces the GWT classes’ bytecode on the fly. This way, the GWT HostedMode is not launched and the execution time is in the order of a few milliseconds. And we can use all the standard tools.
For example, we can use Easymock to test a call to a GWT-RPC service:
1 static interface MyRemoteService extends RemoteService { 2 String myMethod(String param1); 3 } 4 5 static class MyGwtClass { 6 public String myValue; 7 8 public void run() { 9 MyRemoteServiceAsync service = GWT.create(MyRemoteService.class); 10 service.myMethod("myParamValue", new AsyncCallbackgt;() {
11 public void onFailure(Throwable caught) {myValue = "error";}
12 public void onSuccess(String result) {myValue = result;}
13 });
14 }
15 }
16
17 @Mock
18 private MyRemoteServiceAsync mockedService;
19
20 @Test
21 public void checkGwtRpcOk() {
22 // Setup
23
24 // mock remote call 25 mockedService.myMethod(EasyMock.eq("myParamValue"), EasyMock.isA(AsyncCallback.class));
26 expectServiceAndCallbackOnSuccess("returnValue");
27
28 replay();
29
30 // Test
31 MyGwtClass gwtClass = new MyGwtClass();
32 gwtClass.myValue = "toto";
33 Assert.assertEquals("toto", gwtClass.myValue);
34 gwtClass.run();
35
36 // Assert 37 verify(); 38 39 Assert.assertEquals("returnValue", gwtClass.myValue);
40 }
Note: the @Mock annotation is similar to the one we can find in Unitils. It is used to declare a mocked object.
2.4 The constraints and non-constraints of this framework
- There is no need to change the design of / redevelop the GWT application in order to make it testable.
- You need to modify the launch command for these unit tests, by adding argument - javaagent:path_to_bootstrap.jar. This has to be done in the IDE settings and/or in the Maven configuration (in the surefire plugin configuration).
- It’s mandatory to use a Java 6 JVM to execute the tests (a Java 5 JVM does not allow you to modify the code of the native methods). This is easy with Eclipse, by changing the JRE execution. With Maven, you only need to change the JVM used by the surefire plugin.
These constraints are not insignificant, however we feel they are outweighed by the advantages of the test framework.
See Gwt-Test-Utils demo1 project for a complete example of a Maven configuration.
2.5 Results
In our project (a 26k line GWT application compiled using JRockit 1.5, tested under Hotspot 1.6) we achieved 85% code coverage with a total of 600 unit tests (14k lines of test code). However, we have concentrated our tests on the controller part of the GWT application, the purpose being not to re-test GWT, but rather to validate the behaviour that we had implemented.
3 Integration test
3.1 First limit
Gwt-Test-Utils framework allows us to test our UI efficiently. The weakness of these tests is that they are unit tests: we are testing the behaviour of a single view. The server part (which receives the GWT-RPC calls) is mocked. The majority of problems we have encountered are on view chaining, with lots of GWT-RPC calls in views.
3.2 Writing integration tests
In our case, the server backend uses Spring. Therefore, we test it by using SpringJUnit4ClassRunner, which starts the entire server backend for us under JUnit. So, by adding a bit of glue, we have "closed the loop": instead of mocking the GWT application server backend, we have connected the UI part to the server backend.
The GWT application and its server are therefore completely started and operational within a unique JVM, and are ready to perform the tests. For example, we can write a test scenario which:
- launches the server backend
- launches the GWT application (by simply calling its EntryPoint)
- stimulates the GWT application, which will call the server backend
- goes through the views
These tests are no longer unit tests; they are true integration tests.
3.3 In practice
No modifications are needed on the server side. We simply added some glue code to connect the GWT application to the Spring part. We already had integration tests that mocked the services consumed by the server such as databases, Webservices and so on. We simply reused this environment.
To simulate the GWT application, we can write some Java. For example:
1MyView myView = (MyView) RootPanel.get(0); 2myView.getButton().click();
It is not very practical; we need to add getters everywhere. Moreover, we will often want to fetch “the 4th checkbox located in table X which is itself located in container Y, itself located in the RootPanel”. This is why we have developed a small language that resembles XPath and provides a mechanism to call a method on a given component.
For example, to reach a label located in the first widget of a container, itself being in the third widget of the RootPanel, we can write:
1 /rootPanel/widget(2)/widget(0)
The containing text of this label, which is normally accessible by the getText() method, is accessible by
1 /rootPanel/widget(2)/widget(0)/text
All this is made possible by a heavy usage of Java reflection.
Test scenarios can be written using these XPaths, in a CSV file. Here is an example of a CSV scenario which:
- Starts the GWT application
- Checks that the content of a label contains 'foo'
- Simulates a click on a button, which calls a Spring service via GWT-RPC, and replaces the label content
- Checks that the content of the label has changed and now contains 'bar'
Here is the scenario:
1 initApp; 2 assertExact;foo;/rootPanel/widget(2)/widget(0)/text 3 click;/rootPanel/widget(2)/widget(1) 4 assertExact;bar;/rootPanel/widget(2)/widget(0)/text
A small amount of code allows us to launch this scenario in JUnit. These tests are therefore executed the same way as the unit tests, but simply take longer to execute (due to the startup of the server part).
The integration test part is also in the Gwt-Test-Utils project. Documentation is provided here.
We could have done about the same thing with Selenium. There are three major differences:
- GWT does not fit well with Selenium due to the component IDs (however, it can be done).
- Our localisation language is, in our opinion, much simpler and more efficient.
- Our tests are launched by JUnit, which means we can launch them from Eclipse or during the Maven build, which makes their execution easier.
3.4 Conclusion
In the context of our project, we have written 900 UI unit tests and about 40 integration tests.
This collection of tests ensures an overall non-regression of all the features of our application:
- The initial investment was not that significant, because the testing system was developed as we built the application.
- The benefits are enormous: in just about one year of development, we have had virtually no regression.
- The maintenance of the scenarios is expensive (a few hours every 15 days), but we feel it is totally justified in comparison to the benefits incurred.
- The complete non-regression testing suite for the application (in a mocked environment) takes only 3 minutes to execute.
- We often do refactoring inside the GWT application without even launching GWT: if the non-regression tests pass, we are sure that nothing is broken.
GWT has turned out to be a UI technology, which, with a few tools, enables us to perform highly advanced tests thus further increasing the productivity of this technology.
The test framework, as well as its documentation, has been published as an open source project, Gwt-Test-Utils.
About the Authors
Bertrand Paquet works for Octo Technology, a French MIS Architect company. Based on the specific needs of his customers he can either be a Technical Leader, an Architect or a Scrum Master. His job is to facilitate Octo Technology’s corporate customers in realizing their development projects successfully. Bertrand’s favorite topics are continuous integration, tests, agile development and team productivity.
Gael Lazzari works as a consultant for Octo Technology. A Java specialist, he works at Octo’s customers’ sites as an Agile developer.