These are some of the challenges to automated testing in GWT.
JSNI
GWT allows you to develop software in Java, but runs in a browser in Javascript. Most of the code that you write will be compiled from Java into Javascript, but parts of GWT itself and potentially some pieces of your code will simply be replaced by a Javascript equivalent, called using JSNI (JavaScript Native Interface). These pieces of code may rely on the capabilities of Javacript or the document-object model exposed by the browser.
For instance, if you were to attempt to match a regular expression against a String using Java, your Java code will use the Java implementation of regular expressions, but the final Javascript version will use Javascript’s regular expression capabilities. This can lead to subtle differences between behaviour the code that you tested and the final code that you’ll deploy, and even to behaviour from one browser to another.
Similarly, most of the GWT widgets are, at their core, tightly coupled to browser objects and events. As a result, they are not perfectly emulated in Java, and almost every GWT widget is built so that it cannot be easily constructed in Java — it will realize it’s not being run in Web Mode and throw an exception immediately. This makes it very difficult to use a simple JUnit test on widgets or on code that interacts directly with widgets.
GWT Compilation is Slow
Parts of the GWT development cycle aren’t terribly fast, particularly compilation. GWT is doing all sorts of great optimizations during compilation, but those optimizations come at a cost to development speed. That means that if you need to compile your application and launch it in order to test it, you might find that the test cycles are taking too long.
Even with those challenges in mind, you have options.
GWTTestCase
GWT explicitly supports automated testing through a test harness can you can use to load a unit of code within the GWT environment: GWTTestCase. This allows you to test your code thoroughly in an environment that is very similar to the one in which your code will be running ‘in production’, one which supports constructing and using GWT widgets directly.
That sounds like a pretty good setup, until you spend some time with it and discover that it’s very slow, a typical problem with integration testing. Due to the non-trivial nature of compiling your Java to Javascript and starting up the full testing environment, hosted browser and all, each test method might take twenty or more seconds. If you have 500 of these tests (which really isn’t that much for a test codebase), you could be talking about a test suite that takes over an hour and a half to complete.
GWTTestCase has a lot in common with integration testing; it’s slow because of the infrastructure required to do it, but the tests have the capability of being fairly thorough and cross-cutting if you’d like them to be. If you do want to do integration testing of your codebase, it may be the best option, since other integration testing tools are not always well-suited to AJAX-heavy applications.
GWTMockUtilities
As a result of the challenges posed by GWTTestCase, many people try and reduce their exposure to GWTTestCase by using it sparingly or not at all. GWTMockUtilities is intended to make it much easier to perform mock tests of classes that interact with GWT widgets. It does a few different things, the most important of which is to allow calls to
GWT.create
to go through without throwing an exception. This allows you to write unit tests of code that interacts with GWT widgets in Java that run much more quickly than tests written using GWTTestCase. However, in order to allow you to mock all the widgets within a GWT view, you’ll need to write your user interface in a way that separates the widgets from the logic sufficiently that you can mock out all of the widgets being used. Isolating one piece of code from another is a very common problem in building testable code. This kind of test isolation can be encouraged using dependency injection, with or without a framework like Google GIN, and with patterns like the Model-View-Presenter pattern.
Model-View-Presenter
In Ray Ryan’s Best Practices for Architecting your GWT App at Google I/O 2009, he suggests reducing the view to something so simple that it doesn’t need to be tested. The display logic and code to connect your model to the controls can then be tested using JUnit rather than something like GWTTestCase, and those tests can be fast. He calls this Model-View-Presenter in reference to Martin Fowler’s article on the subject.
Martin Fowler has since sub-divided his MVP pattern into two possible implementation styles: Supervising Controller and Passive View. Supervising Controller puts all the event-processing logic and anything complex in the presenter, but allows the View to still perform simple data model bindings. Passive View takes on even those responsibilities, leaving the view to exist primarily as a composite set of user interface widgets. The Passive View style seems more in keeping with the approach Ray Ryan describes in his talk, although if you chose not to test data bindings, then Supervising Controller might be another approach worth considering.
By way of example, let’s imagine a simple login form that looks like this:
This might be represented by a view class like this:
And the presenter would listen for the click event of the button, then get the text from the username and password controls, attempt a login, and then update the view on the basis of the results of the login attempt:
Exposing Widgets through Interfaces
While the Model-View-Presenter pattern can help you make better use of
GWTMockUtilities
, it’s also true that once you’ve established a clean separation of the view from the presenter, you can often skip GWTMockUtilities
altogether by exposing interfaces that represent your widgets from within the view. Mocking interfaces is often a preferred approach, and this is the approach that Ray Ryan demonstrates in his presentation.Instead of having your presenter access a
Button
from the view, it could access a control that implements HasClickHandlers
. TextBox
controls could be replaced with the HasText
interface: In addition to allowing you to avoid the additional complexity of
GWTMockUtilities
, this also reduces the coupling between view and presenter, allowing you to, for instance, replace a Button
with an Image
or an Anchor
as long as it implements the same interface that you’re exposing.Although GWT has a number of interfaces that you can expose, you’ll find there will be methods that you wish to expose that are not covered by an interface. For instance, if you need to change the styles associated with a widget, you’ll find that
addStyleName
and removeStyleName
are not covered by a GWT interface. If you wish to follow this approach, you’ll find you need to introduce new interfaces to GWT, as follows:You’ll also find that although granular interfaces communicate your intent most clearly and increase your ability to substitute implementations, they break down as your interaction between view and presenter increases. If you want to get/set the text of a control as well as adjust the styles, you either need to expose both HasText and your new HasStyle interface, or you need to combine them into a common HasTextAndStyle interface. You may quickly find that the number of combinations you need grows nearly without bounds, and that you’re better off introducing an interface for each widget that exposes all of the methods that you’re likely to invoke on that widget. You can do this with your own set of packages or with a nomenclature like
IButton
for the Button
interface. If you find yourself wanting to substitute one widget for another (see Liskov’s Substitution Principle), you may need a hierarchy of interfaces, possibly mirroring the hierarchy that GWT provides (UIObject, Widget, FocusWidget, etc.). The tradeoff between granular interfaces at the one end of the spectrum and monolithic ones at the other is awkward, and different teams are likely to take different positions on this point.
It’s worth noting that GWT Testing Best Practices at Google I/O encourages a slightly different style of integrating presenter event handlers with view widgets. Where Ray Ryan suggests:
getLogin().addClickHandler( loginHandler );
Daniel Danilatos suggests:
setLoginHandler( loginHandler );
I’ve used the former style in the above examples, but either approach is viable.
Passive View
Passive View makes it possible to assemble complicated GWT user-interfaces and yet still test the bulk of the logic within the user interface. By turning the view into a simpler container for UI widgets, you create a very simple object which is hard to test, but for which automated testing would be of limited value.
The presenter takes on the responsibilities of moving data in and out of those widgets and reacting to events from the user or other parts of the application, and the presenter can be tested using standard Java testing frameworks which run quite quickly. At that point, the choice of testing framework and extensions (JUnit, TestNG, JMock, EasyMock, Mockito) is mostly a matter of taste.
Unfortunately, this kind of separation of the user interface widgets from all the logic that interacts with it has some costs. The view and the presenter are very tightly coupled and have lots of individual interactions. While the sequence diagram for the login view probably seemed reasonable, a complicated view might need to expose tens of widgets, or possibly a dynamic collection of widgets depending on how much information has been displayed on the screen. Each of those widgets may also have a relatively high number of interactions — setting and getting text values, adding and removing styles, showing and hiding the widget, adding click handlers, and so forth.
In an object-oriented application, you typically use encapsulation to contain complexity and reduce the number of external interactions by combining portions of the view into components with their own presentation logic. The Passive View approach can make encapsulation difficult and increase the number of external interactions. This is not simply a matter of design aesthetics — it also means that presenter tests can themselves be quite complicated and require quite a bit of painstaking mocking and stubbing. This may still be preferable to a glacial test suite, but it’s time-consuming and brittle.
If you’ve used other object-oriented user interface frameworks in Java, such as Tapestry or Wicket, you’ll find yourself wanting to encapsulate elements within your pages as components that bundle user interface widgets with logic. It’s certainly possible to do so, but well-encapsulated component that contains both logic and GWT widgets is no longer amenable to simple JUnit testing and you’ll have fall back to the
GWTTestCase
approach. Achieving encapsulation often requires a parallel hierarchy of views and presenters at each level. Orchestrating and wiring these all can be challenging, but it’s still usually better than the alternatives.Decomposition for Testable Views
If you find that you need a complicated view, you might find some scenarios where a portion of the view can be isolated from GWT widgets and remain testable. GWT Testing Best Practices uses a good example where a view implements paging using a high-level view and two lower-level elements, one of which uses widgets and the other is a simple POJO storage model. The high-level view and POJO detail view remain testable, while the widget-detail view, which deals directly with GWT widgets, cannot easily be tested using a simple JUnit test. This sort of decomposition is only likely to work with larger, more-complex views, but it’s something to keep in mind.
Combining MVP and GWTTestCase
There are times where you can develop a sort of hybrid model that allows you to encapsulate elements of the presenter logic into smaller classes which are themselves like miniature presenters but for a component rather than a view, which offer some of the benefits of both encapsulated code and testability, but it’s difficult to develop an approach that you can apply consistently in this vein.
Finally, you can mix these two strategies together, making a conscious decision about when you’d like to build encapsulated components that require slower testing approaches and when you’d like to achieve fast, testable presenter code that reduces your ability to encapsulate and makes for complicated test harnesses. For instance, you could test widget code, particularly custom widgets, using
GWTTestCase
but use Passive Views and test your presenters using JUnit and Mockito or JMock.In Summary
There’s no silver bullet to writing testable applications in Google Web Toolkit. There are several common approaches, each with their own set of tradeoffs, and it’s likely that anyone wishing to build test-driven applications with GWT is going to have their own set of complaints, but forearmed with the knowledge of these approaches, you can reduce the pain that you experience while developing a well-tested GWT application.
How about the "delegate" approach? (the presenter implements a "delegate" or "presenter" interface that the view knows about, and register with the view so that the presenter knows the view, and the view knows the presenter back).
ReplyDeleteSee the Large scale application development and MVP - Part II tutorial, where the interface is called "Presenter".
The net advantage is that you no longer have to think about those IButton, HasStyle, HasText, HasClickHandlers, etc. interfaces.
What about the "gwt-test-utils" tool (http://code.google.com/p/gwt-test-utils)?
ReplyDeletePersonally, I consider MVP a choice of last resort. It tends to add *a lot* of unnecessary code.
If one implements activities and places framework then the activity now is presenter + activity. How will one JUnit test the logic inside the activity alone? the challenges are The contructor of activity requires Place & ClientFactory. Also if I have a private method which should be called inside start() method to do some initial processing then I can't test that private method unless I call start() from my JUnit test class and the problem is start() method requires AcceptsOneWidget & EventBus parameters.
ReplyDeleteWould love to hear from you if you know a way to answer this.