Test driven development for GWT UI code with asynchronous RPC

by Rob van MarisApril 22, 2008

In my previous blog post I we saw a test-driven approach to GWT UI code. This was based on moving logic to a Supervising Controller.
In this follow-up post we’ll turn our attention to interacting with RPC, and see how this can be tested using EasyMock. This involves some tricks and non-trivial boilerplate code, but fortunately most of this can be hidden by introducing convenience methods, and we end up with tests that are expressive and easy to read.
This article continuous on the sample code in my previous blog post, but if you’re just interested in the RPC part you can probably skip that and start from here. Familiarity with EasyMock basics is assumed.

The sample application

Let’s extend the sample application introduced in my previous blog post. The image below shows what it will look like in hosted mode. I have added a button “Random” next to the HFL input field.

The sample application

Pressing the button should have this effect:

  1. Disable the button
  2. Perform an RPC call to the server to retrieve a random float value
  3. If the RPC call fails: ignore the exception and re-enable the button
  4. If the RPC call returns successfully: update the HFL amount in the model and re-enable the button. The model update will result in the EUR amount to be recalculated, and both fields in the UI being updated

The RPC service

Here’s the RPC service interface

public interface ConverterService extends RemoteService {
    float getRandomFloat();
}

And its asynchronous variant for use by the client:

public interface ConverterServiceAsync {
    void getRandomFloat(AsyncCallback async);
}

For now we just want to test the interaction of the client with the (async) service. Testing the actual implementation of the service is something that can be done independently. That’s outside the scope of this article.

The controller code

The behaviour described above is implemented by adding this method to the controller, that registers the button:

    public void registerButton(final ButtonInput button) {
        button.addClickListener(new ClickListener() {
            public void onClick(Widget sender) {
                button.setEnabled(false);
                converterService.getRandomFloat(new AsyncCallback() {
                    public void onFailure(Throwable caught) {
                        button.setEnabled(true);
                    }

                    public void onSuccess(Object result) {
                        converterModel.updateAmountHfl(((Float) result).floatValue());
                        button.setEnabled(true);
                    }
                });
            }
        });
    }

So far it’s pretty straightforward. A call to the updateAmountHfl() method on the model is sufficient to update the model. Code is already in place to update the amounts in the UI when the model changes, as shown in my previous blog post.

What’s a ButtonInput? It’s an interface for the button widget, that allows us to replace the widget by mock buttons in the unittests, outside of an GWT environment. This technique was explained in the previous blog post.

Testing strategy

Now lets test the controller. Let’s start with the “happy path”, i.e. assuming a successful RPC call:

import static org.easymock.EasyMock.*;

public class ConverterControllerTest extends TestCase {
    private ConverterController instance;

    private ConverterModel model;
    private TextInputMock hflTextInput = new TextInputMock();
    private TextInputMock eurTextInput = new TextInputMock();
    private ButtonInputMock button = new ButtonInputMock();

    private ConverterServiceAsync converterServiceMock =
        createMock(ConverterServiceAsync.class);

    protected void setUp() throws Exception {
        model = new ConverterModel();
        instance = new ConverterController(model, converterServiceMock);
        instance.registerButton(button);
        model.fireModelChangedEvent();
    }

    /**
     * Pass condition: RPC service is called, on success the model and view
     * are updated accordingly.
     */
    public void testRegisterButton_success() throws Exception {
        converterServiceMock.getRandomFloat(isA(AsyncCallback.class));
        expectLastCall().andAnswer(new IAnswer() {
            public Object answer() throws Throwable {
                final Object[] arguments = getCurrentArguments();
                AsyncCallback callback =
                        (AsyncCallback) arguments[arguments.length - 1];
                callback.onSuccess(10.F);
                return null;
            }
        });

        replay(converterServiceMock);

        button.fireClick();

        verify(converterServiceMock);

        assertEquals(10.0f, model.getAmountHfl());
        assertEquals("10.0", hflTextInput.getText());
        assertEquals(4.537802f, model.getAmountEur());
        assertEquals("4.537802", eurTextInput.getText());
    }

    // More tests...
}

We use EasyMock to create a mock service instance, converterServiceMock. The test itself is implemented in testRegisterButton_success():

  1. record the expected call on the service method getRandomFloat() with an unknown parameter of type AsyncCallback.
  2. register that in response to this expected call onSuccess() is called on the AsyncCallback object. This completes mocking the RPC call.
  3. simulate a click on the button
  4. make sure the service was called as expected
  5. verify the model and the text input fields have the expected values afterward

The code is straightforward, except for the part that uses andAnser() to handle the call of onSuccess() on the AsyncCallback object that is passed in to the mock service. That part may appear a bit obscure (read the EasyMock documentation to see how it works) and is mostly boilerplate. It seems to be good idea to move it to a convenience method in a separate class.

import com.google.gwt.user.client.rpc.AsyncCallback;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.getCurrentArguments;
import org.easymock.IAnswer;

/**
 * @author Rob van Maris
 */
public class AsyncCallbackMockSupport {
    /**
     * Calls onSuccess() on the AsyncCallback passed in with mocked RPC call,
     * i.e. the RPC call that was recorded on the mock immediately before this
     * method was called.
     */
    public static void expectLastCallAsyncSuccess(final Object result) {
        expectLastCall().andAnswer(new IAnswer() {
            public Object answer() throws Throwable {
                final Object[] arguments = getCurrentArguments();
                AsyncCallback callback =
                        (AsyncCallback) arguments[arguments.length - 1];
                callback.onSuccess(result);
                return null;
            }
        });
    }

    /**
     * Calls onFailure() on the AsyncCallback passed in with mocked RPC call,
     * i.e. the RPC call that was recorded on the mock immediately before this
     * method was called.
     */
    public static void expectLastCallAsyncFailure(final Throwable caught) {
        expectLastCall().andAnswer(new IAnswer() {
            public Object answer() throws Throwable {
                final Object[] arguments = getCurrentArguments();
                AsyncCallback callback =
                        (AsyncCallback) arguments[arguments.length - 1];
                callback.onFailure(caught);
                return null;
            }
        });
    }
}

Note there is a similar method to mock a failing RPC call.

Taking advantage of the Java 5 static import feature, tests can now use convenience methods like AsyncCallbackMockSupport.expectLastCallAsyncSuccess() in a similar fashion as EasyMock methods like EasyMock.expectLastCall():

import static nl.robvanmaris.gwtqs.client.rpc.AsyncCallbackMockSupport.*;

    // ...

    public void testRegisterButton_success() throws Exception {
        converterServiceMock.getRandomFloat(isA(AsyncCallback.class));
        expectLastCallAsyncSuccess(10.F);

        replay(converterServiceMock);

        button.fireClick();

        verify(converterServiceMock);

        assertEquals(10.0f, model.getAmountHfl());
        assertEquals("10.0", hflTextInput.getText());
        assertEquals(4.537802f, model.getAmountEur());
        assertEquals("4.537802", eurTextInput.getText());
    }

    // ...

I’m quite happy with the code for this test: it expresses well what is being tested. But there is still something missing. In this test, the callback is called immediately after the call to the service. This does not reflect the asynchronous nature of RPC calls. We cannot test behaviour between start and completion of the RPC call. For instance we’re not able to test that the button is disabled until the RPC call completes.

Testing asynchronous callback

To truly mock the asynchoronous call, we need a reference to the AsyncCallback object, that we can call whenever we like after the expected call to the service mock has occurred. We can achieve this by introducing a proxy for the callback. Let’s add these lines to AsyncCallbackMockSupport:

    /**
     * Returns proxy for the AsyncCallback passed in with mocked RPC call,
     * i.e. the RPC call that was recorded on the mock immediately before
     * this method was called.
     *
     * @return the proxy
     */
    public static AsyncCallback expectLastCallAsync() {
        final AsyncCallbackProxy callbackProxy = new AsyncCallbackProxy();
        expectLastCall().andAnswer(new IAnswer() {
            public Object answer() throws Throwable {
                final Object[] arguments = getCurrentArguments();
                AsyncCallback callback =
                        (AsyncCallback) arguments[arguments.length - 1];
                callbackProxy.setCallback(callback);
                return null;
            }
        });
        return callbackProxy;
    }

    /**
     * Proxy for async callback object.
     *
     * @author Rob van Maris
     */
    private static class AsyncCallbackProxy implements AsyncCallback {
        private AsyncCallback callback;

        public void onFailure(Throwable caught) {
            callback.onFailure(caught);
        }

        public void onSuccess(Object result) {
            callback.onSuccess(result);
        }

        private void setCallback(AsyncCallback callback) {
            this.callback = callback;
        }
    }

Here’s the improved test:

    /**
     * Pass condition: button is disabled until call returns, on success
     * the model and view are updated accordingly.
     */
    public void testRegisterButton_success() throws Exception {
        converterServiceMock.getRandomFloat(isA(AsyncCallback.class));
        AsyncCallback callback = expectLastCallAsync();

        replay(converterServiceMock);

        button.fireClick();
        assertFalse(button.isEnabled());
        callback.onSuccess(10.F);
        assertTrue(button.isEnabled());

        verify(converterServiceMock);

        assertEquals(10.0f, model.getAmountHfl());
        assertEquals("10.0", hflTextInput.getText());
        assertEquals(4.537802f, model.getAmountEur());
        assertEquals("4.537802", eurTextInput.getText());
    }

Now, at last, I’m perfectly happy with the code. It expresses well what is being tested: when the button is clicked, the button is disabled and the RPC servicemethod is called. After the call returns successfully, the button is re-enabled, and model and view are updated according to the result of the call.

In addition we have to test for behaviour when the RPC call fails:

    /**
     * Pass condition: button is disabled until call returns, on failure
     * the model and view are not updated.
     */
    public void testRegisterButton_failure() throws Exception {
        hflTextInput.setText("");
        eurTextInput.setText("");

        converterServiceMock.getRandomFloat(isA(AsyncCallback.class));
        AsyncCallback callback = expectLastCallAsync();

        replay(converterServiceMock);

        button.fireClick();
        assertFalse(button.isEnabled());
        callback.onFailure(new RuntimeException());
        assertTrue(button.isEnabled());

        verify(converterServiceMock);

        assertEquals("", hflTextInput.getText());
        assertEquals("", eurTextInput.getText());
    }

Conclusion

The sample code demonstrates that RPC calls in controllers can be mocked in unittests. The code of the tests has become expressive and concise, thanks to the introduction of a few convenience methods. Combining this with the approach in my previous blog, a full TDD approach to GWT UI code can be achieved.
This is TDD Nirvana: almost all logic can be implemented and tested without leaving the IDE. Deployment to server, browser or hosted mode is needed every now and then, but only for testing the layout, the configuration and integrationtests.
You save time on deployment, and invest in test coverage.