📜 ⬆️ ⬇️

Testing GWT MVP Architecture Applications

Good day!

In this article, I will review unit / integration testing in GWT using the GWT UI components and GXT and MVP (with Passive View) architectures to separate the logic and appearance of the application.
GWT and GXT are not randomly highlighted here - Google has developed several frameworks that facilitate the support of the MVP pattern (more precisely, the more general, the separation of logic and presentation) in GWT. These are Activity and Place for dividing application logic into modules, GWT Editor for automatic mapping of POJO objects to widget, UiBinding for declarative interface description.
All this is also supported by the GXT UI framework. T.ch. essentially there won't be much difference in using the UI of the GWT or GXT components.

As a result, we get an easily tested application without lifting a heavy GWT framework.

purpose


Just write tests on the GWT application with maximum functionality coverage. Ideally, a simple writing of integration tests (integration tests would immediately cover all application layers with a minimum number of tests, thereby testing the interconnection of layers). It is necessary, because A lot of logic is on the client and I would also like to cover it to the maximum with simple, easy-to-run tests that can be quickly launched on the developer's machine.
')

Used technologies and frameworks


1) MVP (Model View Presenter) with Passive View is the cornerstone in testing applications with UI. In short, this is a pattern of separation of logic and display in which the View has a minimal role. Due to this, View becomes simple and it will be more difficult to make mistakes in it. And since it is difficult to test it (for this, the whole environment of the GWT will have to be raised), this will only be at hand - we will not test it.

For details on MVP look here:
MVP to GWT. Official documentation: www.gwtproject.org/articles/mvp-architecture.html
Passive View. M. Fowler: martinfowler.com/eaaDev/PassiveScreen.html
MVP in GWT: www.javabeat.net/what-is-model-view-presenter-mvp-in-gwt-application
Various MV * patterns: outcoldman.com/ru/archive/2010/02/22/patterns-mvc-mvp--mvvm

2) DI (Dependence Injection) - implementation for GWT - GIN, for testing without launching GWT - Guice. All dependencies in Presenters instead of GWT.create () are defined using Inject annotation. Those. when using GWT RPC instead
ServiceAsync serviceAsync = GWT.create(ServiceAsync.class); 

write
  @Inject ServiceAsync serviceAsync; 

It is also necessary when testing without completely raising the GWT environment - while dependencies that are not possible to create (for example, the implementation of the View) will be replaced with mocks. Plus the use of GIN and Guice in a pair - they use some annotations to determine the DI (Inject, Provide), so the code does not need to be changed under a different DI framework and you don’t need to configure anything else - just correctly bind the dependencies.

3) Mocking - emulation of objects. It is necessary for replacement, at least View, for Presenters to work correctly. As a framework for mocking, we use Mockito.

4) SyncProxy - framework for executing GWT RPC requests from Java. It is necessary for testing, to provide the ability to make GWT RPC calls without raising the GWT environment.
SyncProxy code.google.com/p/gwt-syncproxy

5) Jetty - servlet container. Need to run a web application.

6) GWT Editor - GWT framework that allows you to automatically map POJO objects to UI components. It is necessary to unload the View from the mapping and thereby bring it closer to the Passive View.

GWT Editor. Official documentation: www.gwtproject.org/doc/latest/DevGuideUiEditors.html
GWT Editor in GXT: docs.sencha.com/gxt/3.1.0-beta/data/Editors.html

7) Activity and Place - a framework from Google for a convenient history mechanism (the history of a page visit in the browser - back and forth buttons). In principle, it is not required here, but makes it easy to implement support for browser history, t.ch. also consider.

Activity and Place. Official documentation. www.gwtproject.org/doc/latest/DevGuideMvpActivitiesAndPlaces.html

8) UiBinder - GWT mechanism for declarative description of UI. In fact, GWT UI elements are defined as in html / jsp pages - with the help of tags. It is also not necessary, but it allows you to unload the View, leaving only event handlers and a minimum of logic.

UiBinder. Official documentation: www.gwtproject.org/doc/latest/DevGuideUiBinder.html

Unused technologies


1) Selenium - tests written with the help of Selenium are difficult to maintain, write and run. Although they allow for full integration testing, they need a separate person who will maintain them in working order and a separate machine on which they can be quickly started. In small teams, if you can select and configure a server to run Selenium tests, then it is very difficult to find an individual to support them. T.ch. I don't consider it, although I used it successfully on other projects (with the QA engineer who wrote the tests).

2) GWTTestCase - GWT JUnit runner, which allows you to raise the GWT environment in JUnit tests and emulate a browser using HtmlUnit. Not used here, because it runs for a long time (in fact, this is the launch of GWT in development mode), and due to the use of HtmlUnit, some JavaScript emulation may not work correctly. In general, this is a half-hearted solution - on the one hand, the GWT is running, but the other is running a long time and will not work correctly in all cases. T.ch. I also did not use it, although for some cases it may come up.

Application Architecture and Highlights


Again, the main pattern facilitates testing in GWT - MVP with Passive View. Its essence is to concentrate all the logic in the Presenter, to make the View as simple as possible (the simpler the code, the fewer errors it has, and since we will not cover View with tests, because it is difficult, this is very important). At the same time, Presenter should not have a UI code - no use of UI elements, calls to GWT.create () (instead, dependency injection using DI is used) and everything that cannot be executed on the JVM. All this is needed so that the Presenter code can be executed on the JVM without compiling it in JS or lifting GWTDevMode, which takes a long time.

Test Launch Stages


1) Raise Jetty with our app.
2) Configure SyncProxy for our GWT RPC services.
3) Using Guice, we define the implementations of the GWT RPC services, Presenters, and View interfaces (with the mock View).
4) Extras. in the case of using Activity and Place, configure ActivityMapper, ActivityManager, PlaceControler.
5) Test Presenters and (if available) Activity and Place.

Sample application


As an example, I made an application for editing students and groups. A student can belong to only one group. In total there are 4 forms - a list of students, a list of groups, viewing / editing a student, viewing / editing a group.

Sources are located here: github.com/TimReset/example.gwt.gxt.test
The application is using maven. In the project folder there is a repo folder in which SyncProxy is located (since it is not in maven) and annotation.jar from IDEA (since I use the NotNull and Nullable annotations from IDEA).

As a base Presenter and View use the following:
 /** *    presenter'.   <a href="http://www.gwtproject.org/articles/mvp-architecture.html"> *   MVP  Google</a>  <a href="http://www.gwtproject.org/doc/latest/DevGuideMvpActivitiesAndPlaces.html"> *   Activity  Place</a>.         MVP.      , *  View     Presenter,       (,      *  View).   ,      callback'  View,      Presenter'  *  . * <p/> * <p/> * Use case  Presenter': * <p/> * Presenter'     -     Presenter'   *  EventBus,   Presenter'  (  inject).  , .. EventBus   *       Presenter'.  Presenter'     Activity. ({@link * BaseActivity ). */ public interface BasePresenter<V extends BasePresenter.View> { /** *  {@link View}  .         View.  * View Presenter     ,     .  ,      *   View,      View.   {@link View},   {@link IsWidget},  *     View   View. * * @return View. */ @NotNull V getWidget(); /** *  View  Presenter'. , ..    {@link #setPresenter(BasePresenter)}  *     Presenter'   View   Presenter'  . */ public interface View<T extends BasePresenter> extends IsWidget { /** * View    Presenter,       View    Presenter'.  *    set ,     Inject, .. 1) GIN     - *  View   Presenter  Presenter   View.     -  * . 2) ,    View Presenter      , .. View     * Presenter,      View,  Presenter    View,    * . * * @param presenter   Presenter   View. */ void setPresenter(@NotNull T presenter); } } 


The essence of the basic Presenter is to determine the main methods for working with Presenters and View: from Presenter, get the View. The method is needed because We do not work directly with the View (because the Presenter is responsible for the View state, and he himself must decide when he sends this View, in which state to give it), but only through the Presenter. View has a method for installing Presenter. The method is needed because you can't make circular dependencies in GIN / Guice - a Presenter instance refers to View and View refers to Presenter.

UML Class Chart



The diagram shows the relationship between the classes — Presenter communicates with View and sends messages to the EventBus. Activity communicates with Presenters (only one Presenter is shown in the diagram but there may be more) and works with EventBus - listens to messages from Presenters and other messages.

The main nuances of interaction between objects:

1) Messages from Presenter to Activity are transmitted via EventBus. As an alternative, you can use callbacks that the Activity will install in the Presenter. But this solution has one drawback - callbacks and you need to remember to erase, so that there are no resource leaks due to the fact that Presenter singleton and Activity is not. Those. Activity installed the Presenter callback. They switched to a new Activity and did not install a callback (and the Presenter forgot to reset it) And then the link to the callback of the old Activity will hang, or worse, this callback will be called. In the Activity and Place mechanism, the GWT when creating an Activity is passed to the ResettableEventBus, which is cleared when switching to a new Activity. T.ch. An activity can set how many event handlers you need without worrying about deleting them. When you change Activity, these handlers will be automatically deleted. And Presenter doesn't need to worry about the presence of callbacks - it simply sends messages to the EventBus.

2) Messages from Activity to Presenter can be directly sent through the methods of the Presenter interface.

3) Presenter with View interact through each other's interfaces. It is necessary that when testing to replace the implementation of the View mock'om.

4) Only Presenter knows about View - all others communicate only with Presenter. Again, it is convenient when replacing View with mock and subsequent dependency tracking (when nobody knows about View and it is easy to change).

Test environment settings


Jetty is set up programmatically in the same way as in web.xml - i.e. All that is written in web.xml is transferable to the method call in Jetty. I did it this way, and did not explicitly specify web.xml because I could not use the relative path to web.xml, and I didn’t want to write tests focused on specific paths - I would have to change them every time. T.ch. if you manage to launch Jetty with a relative path to web.xml, it will be fine.

To configure SyncProxy, we use a Map with a list of asynchronous GWT RPC services interfaces, their implementations (needed for easy launching of GWT RPC servlets in Jetty) and the paths to these servlets.

 /** *  GWT RPC .  -  GWT RPC .  -      -  *   web.xml.     GWT RPC   Jetty     *  -        Jetty,     SyncProxy  *    Guice. */ private static final Map<Class<? extends BaseRemoteService>, Pair<Class<?>, String>> gwtRpcServlets = Collections .unmodifiableMap(new HashMap<Class<? extends BaseRemoteService>, Pair<Class<?>, String>>() { private static final long serialVersionUID = -2126682232601937926L; { put(StudentsServiceImpl.class, new Pair<Class<?>, String>(StudentsServiceAsync.class, "/GxtModule/students")); put(GroupsServiceImpl.class, new Pair<Class<?>, String>(GroupsServiceAsync.class, "/GxtModule/groups")); } }); 


Actually the mapping itself occurs in this cycle:

 for (Map.Entry<Class<? extends BaseRemoteService>, Pair<Class<?>, String>> entry : gwtRpcServlets .entrySet()) { // Cookie   ,    SyncProxy   , .. http   . //      (waitForInvocation = true). ,      -       . gwtRpcAsyncInstances.put(entry.getValue().getA(), SyncProxy.newProxyInstance(entry.getValue().getA(), URL_TO_SERVER, entry.getValue().getB(), true)); } 


I draw your attention to the following points when using SyncProxy:

1) Asynchronous interface instances are stored in Map gwtRpcAsyncInstances. It is necessary that, in consequence, it is universal to get them when bind.

2) A synchronous mechanism is used in asynchronous interfaces. This means that when you call an asynchronous GWT RPC method, the next line of code will not be executed until the response from the server comes (i.e., the methods com.google.gwt.user.client.rpc.AsyncCallback # onFailure or com.google .gwt.user.client.rpc.AsyncCallback # onSuccess This makes writing tests very easy - in fact, the tests will be written in steps as synchronous and there will be no need to further process the waiting for the server to respond.

3) It is necessary in the test package to create a copy of the com.google.gwt.user.server.rpc.impl.LegacySerializationPolicy class and override the com.google.gwt.user.server.rpc.impl.LegacySerializationPolicy # isInstantiable method to support the classes heirs are serializable. T.ch. The method code should end up with the following:
  private boolean isInstantiable(Class<?> clazz) { if (clazz.isPrimitive()) { return true; } if (clazz.isArray()) { return isInstantiable(clazz.getComponentType()); } //   Serializable.class.isAssignableFrom(clazz) if (IsSerializable.class.isAssignableFrom(clazz) || Serializable.class.isAssignableFrom(clazz)) { return true; } return SerializabilityUtil.hasCustomFieldSerializer(clazz) != null; } 


This is necessary because The default GWT RPC, if there is no file with the GWT Policy (file describing serializable types), uses the LegacySerializationPolicy to check whether the object can be serialized. And in this class there is no support for the Serializable interface. T.ch. will have to add it manually. It is this method override that is used (with a complete replacement of the class), since GWT RPC directly creates an instance of this class and you cannot specify the GWT RPC for your Serializable Policy.

And the final point of tuning is the definition of bind's in Guice. To define Presenters, Map is also used with their Presenter class, Presenter implementation class and View classes of this Presenter.
 /** *  Presenter',    View.    . */ private static final Map<Class<? extends BasePresenter>, Pair<Class<? extends BasePresenter>, Class<? extends BasePresenter.View>>> presenters = Collections.unmodifiableMap(new HashMap<Class<? extends BasePresenter>, Pair<Class<? extends BasePresenter>, Class<? extends BasePresenter.View>>>() { private static final long serialVersionUID = 3512350621073004110L; private <T extends BasePresenter> void addPresenter(Class<T> presenterInterface, Class<? extends T> implementPresenter, Class<? extends BasePresenter.View<T>> viewClass) { if (!presenterInterface.isInterface()) { throw new IllegalArgumentException("Should be interface " + presenterInterface.getName()); } if (implementPresenter.isInterface()) { throw new IllegalArgumentException("Should be class " + implementPresenter.getName()); } put(presenterInterface, new Pair<Class<? extends BasePresenter>, Class<? extends BasePresenter.View>>(implementPresenter, viewClass)); } { addPresenter(StudentPresenter.class, StudentPresenterImpl.class, StudentPresenter.View.class); addPresenter(StudentsListPresenter.class, StudentsListPresenterImpl.class, StudentsListPresenter.View.class); addPresenter(MainWindowPresenter.class, MainWindowPresenterImpl.class, MainWindowPresenter.View.class); } }); 


Next, bind'im the GWT RPC asynchronous interface objects created by SyncProxy, Presenters, with the automatic creation of a mock for View, EventBus and, in our case, we raise objects for the Activity and Place to work. I draw your attention to the fact that everything here is defined as a singleton. For Presenters and View, this is not critical, but EventBus and classes associated with Activity and Place should be just that - because they are used in many places and have links to each other.

  injector = Guice.createInjector(new AbstractModule() { @Override protected void configure() { //     . for (Class gwtRpcAsyncClass : gwtRpcAsyncInstances.keySet()) { bind(gwtRpcAsyncClass).toInstance(getGwtRpc(gwtRpcAsyncClass)); } for (Map.Entry<Class<? extends BasePresenter>, Pair<Class<? extends BasePresenter>, Class<? extends BasePresenter.View>>> entry : presenters.entrySet()) { log.info("Bind View {}", entry.getValue().getB().getName()); bindMock(entry.getValue().getB()); log.info("Bind Presenter {} to implementation {} ", entry.getKey().getName(), entry.getValue().getA().getName()); bind(entry.getKey()).to((Class) entry.getValue().getA()).in(Singleton.class); } EventBus eventBus = new SimpleEventBus(); bind(EventBus.class).toInstance(eventBus); com.google.gwt.place.shared.PlaceController placeController = new com.google.gwt.place.shared.PlaceController( eventBus, new com.google.gwt.place.shared.PlaceController.Delegate() { //   Delegate, , .. Delegate    UI. }); bind(com.google.gwt.place.shared.PlaceController.class).toInstance(placeController); bind(ActivityMapper.class).to(ru.timreset.example.gxt.client.ActivityMapper.class).in(Singleton.class); bind(AcceptsOneWidget.class).to(ru.timreset.example.test.base.AcceptsOneWidget.class).in(Singleton.class); } /** *   mock        . * @param bindClass  mock . */ private void bindMock(Class bindClass) { Object bindObject = Mockito.mock(bindClass); bind(bindClass).toInstance(bindObject); } }); ActivityManager activityManager = new ActivityManager(getInstance(ActivityMapper.class), injector.getInstance( EventBus.class)); activityManager.setDisplay(getInstance(AcceptsOneWidget.class)); final PlaceHistoryHandler historyHandler = new PlaceHistoryHandler( new PlaceHistoryMapper(), new PlaceHistoryHandler.Historian() { //   Historian, , .. Historian    UI    . }); historyHandler.register(injector.getInstance(com.google.gwt.place.shared.PlaceController.class), injector.getInstance(EventBus.class), Place.NOWHERE); historyHandler.handleCurrentHistory(); 


Sample Presenter by StudentPresenter


 /** * Presenter    .   // . */ public interface StudentPresenter extends BasePresenter<StudentPresenter.View> { /** *   Presenter'  . * * @param mode   Presenter'. * @param studentId id . * @param eventBus EventBus. * @param onReady Callback       Presenter'. */ void init(@NotNull Mode mode, @Nullable Integer studentId, @NotNull EventBus eventBus, @NotNull Command onReady); /** *   . * * @param student . */ void saveStudent(Student student); /** *    . * * @return true -  , false -  . */ boolean isEdit(); /** *    . */ void onEditStudent(); /** *    . * * @return  . */ Mode getMode(); /** *     . * * @return true -  , false -   . */ boolean isDirty(); /** *   Presenter'. */ enum Mode { /** * . */ VIEW, /** * . */ EDIT, /** * . */ CREATE; } interface View extends BasePresenter.View<StudentPresenter> { /** *    . * * @param student . */ void setStudent(Student student); /** *      . * * @return true -  , false -   . */ boolean isDirty(); } } 


Here are the methods that must be implemented by View and Presenter.

Messages from Presenter to View go through the StudentPresenter.View interface. This is the usual MVP scheme. Messages from View to Presenter go through the StudentPresenter interface. In many MVP examples, they do not do this — usually, in the View interface, methods for setting callbacks (eg setEditClick (Callback c)) and Presenter are done when the View is initialized, set the callback to it (eg view.setEditClick (new Callback () {/ * handler function * /})). View should, in the implementation of methods for setting callbacks, make them retain at their place and at the right moment should call them. I did not like this decision because there will be a lot of similar code for saving callbacks and in View there will be many fields with these callbacks. And in Presenter there will be many anonymous classes with the code of these callbacks that are not read very well.

That's why I chose the option when the View has a link to the Presenter interface and when it is necessary to transfer a message from View to the Presenter, the View simply calls the Presenter methods. In this implementation, the minus of this solution is that the Presenter interface actually contains 2 types of methods - methods that are needed for interaction between the View and Presenter (in the example above, these are saveStudent, onEditStudent, getMode, isEdit) and are needed for Presenter to interact with the outside world (these are the init methods, isDirty). To avoid this minus, you need to put the methods of View interaction with Presenter in a separate interface and View to transfer an instance of this interface. In fact, this interface can be implemented by the same class as the StudentPresenter interface.

An example with an additional interface

 public interface StudentPresenter extends BasePresenter<StudentPresenter.View> { void init(@NotNull Mode mode, @Nullable Integer studentId, @NotNull EventBus eventBus, @NotNull Command onReady); boolean isDirty(); enum Mode { VIEW, EDIT, CREATE; } /** *     View  Presenter'. */ interface ViewPresenter extends BasePresenter.ViewPresenter{ void saveStudent(Student student); boolean isEdit(); void onEditStudent(); Mode getMode(); } interface View extends BasePresenter.View<StudentPresenter.ViewPresenter> { void setStudent(Student student); boolean isDirty(); } } 


In this case, Presenter consumers will see only the methods intended for them, and not the internal methods for interacting with the View.

, C# View Presenter event' — .









:
 /** *    Place  . * * @param newPlace  Place. */ void goToWithAssert(Place newPlace) /** * ,     Place. ! Place     {@link * Object#equals(Object)}.      . * * @param expectedWhere   (Place). */ void assertWhere(Place expectedWhere); /** * ,    View  Presenter'. ..  ,       * Presenter. * * @param presenter Presenter, View      . */ void assertWhere(BasePresenter presenter); 


, .

. Presenter' . ru.timreset.example.gxt.client.presenter.StudentPresenter
 @Test public void listStudent() { //  Presenter  . MainWindowPresenter mainWindowPresenter = getInstance(MainWindowPresenter.class); //  Presenter  . StudentsListPresenter studentsListPresenter = getInstance(StudentsListPresenter.class); //     . mainWindowPresenter.goToStudentsList(); // ,   . assertWhere(studentsListPresenter); //   . studentsListPresenter.getStudents(new PagingLoadConfigBean(0, 999), new ru.timreset.example.gxt.client.AsyncCallback<PagingLoadResult<Student>>() { @Override public void onSuccess(PagingLoadResult<Student> result) { //,   . Assert.assertFalse(result.getData().isEmpty()); } }); } 


This is a trivial example, but it already checks the work of the main menu and the list of students.

Now consider the example more difficult - the creation of a student. When creating a student, we check the complete chain
1) Click on the "List of students" in the menu.
2) Click on "Create Student" in the list of students.
3) Fill in the fields in the student creation window.
3) Save the student.
4) Check that the saved student is in the list.

At the same time, at each step, we check that the necessary forms and lists are displayed.

 @Test public void editTest() { //  Presenter  . MainWindowPresenter mainWindowPresenter = getInstance(MainWindowPresenter.class); //  Presenter  . StudentsListPresenter studentsListPresenter = getInstance(StudentsListPresenter.class); //  Presenter  . StudentPresenter studentPresenter = getInstance(StudentPresenter.class); // View  . StudentPresenter.View studentView = getInstance(StudentPresenter.View.class); //   mainWindowPresenter.goToStudentsList(); // ,   .   Place  View. assertWhere(StudentsListPlace.buildList()); assertWhere(studentsListPresenter); //    . studentsListPresenter.onCreateStudent(); //     . assertWhere(StudentPlace.buildCreate()); //      . assertWhere(studentPresenter); //     . Assert.assertEquals(StudentPresenter.Mode.CREATE, studentPresenter.getMode()); //   ArgumentCaptor<Student> studentArgumentCaptor = ArgumentCaptor.forClass(Student.class); Mockito.verify(studentView).setStudent(studentArgumentCaptor.capture()); //  . Student student = studentArgumentCaptor.getValue(); student.setName("TEST_NAME"); student.setSurname("TEST_SURNAME"); student.setPatronymic("TEST_PATRONYMIC"); student.setBirthday(new Date()); student.setStudentType(StudentType.ABSENTED); //  studentPresenter.saveStudent(student); //      . assertWhere(StudentsListPlace.buildList()); assertWhere(studentsListPresenter); //    ,      . studentsListPresenter.getStudents(new PagingLoadConfigBean(0, 999), new ru.timreset.example.gxt.client.AsyncCallback<PagingLoadResult<Student>>() { @Override public void onSuccess(PagingLoadResult<Student> result) { Collection<Student> coll = Collections2.filter(result.getData(), new Predicate<Student>() { @Override public boolean apply(Student input) { return "TEST_NAME".equals(input.getName()); } }); //,   . Assert.assertEquals(1, coll.size()); } }); } 


. . « ». , « ». (, View) Place. . , .


Conclusion


, — MVP . GWT Editor, UiBinder, Activity and Place. . . ( ), Presenter' .


Pros:
1) «UI — — — ».
2) ( — ).
3) UI .

Limitations:
1) UI . , , , click handler' Presenter'. View, .
2) MVP — , overhead MVP.

— , .. ( MVP) ( , GWT ).


GWT Mockito
www.objectpartners.com/2013/11/07/testing-gwt-with-gwtmockito

Unit and Integration Testing for GWT Applications
www.infoq.com/articles/gwt_unit_testing

Source: https://habr.com/ru/post/246285/


All Articles