Running integration tests against a GWT RequestFactory based back-end

by Stefan

Update 20.07.2011: Added post.releaseConnection() class to HttpPostTransport

If your app uses GWT’s RequestFactory capabilities, a well-defined external interface to your back-end is automatically exposed. The definition is provided using the RequestContext interfaces that are mapped to a service running in the back-end. Example:

Back-end service:

package cleancodematters.server;

import cleancodematters.server.domain.Pizza;

public interface PizzaDao {
  void save( Pizza pizza );
  Pizza findById( Long id );
}

RequestFactory with RequestContext (see the first tutorial for the full code):

package cleancodematters.client;

import cleancodematters.server.DaoLocator;
import cleancodematters.server.PizzaDao;

import com.google.web.bindery.requestfactory.shared.Request;
import com.google.web.bindery.requestfactory.shared.RequestContext;
import com.google.web.bindery.requestfactory.shared.RequestFactory;
import com.google.web.bindery.requestfactory.shared.Service;

public interface PizzaRequestFactory extends RequestFactory {

  @Service(value = PizzaDao.class, locator = DaoLocator.class)
  public interface PizzaRequestContext extends RequestContext {
    Request findById( Long id );
    Request save( PizzaProxy pizza );
  }

  PizzaRequestContext context();
}

With this infrastructure, not only the JS-based client running in the browser but, in fact, any Java client can communicate with your back-end. Although this might not be the full fledged interface for integration with other systems, it is perfectly suitable for running any kind of headless tests against your back-end. This can be functional tests as well as load and performance tests.

All we need to do is to replace the default XHR-based transport layer with a pure JRE compatible implementation. In this example, we use the Apache HttpClient lib. Similar to the tutorial on unit testing a helper class provides a factory method to create RequestFactory instances:

package cleancodematters;

import com.google.web.bindery.event.shared.SimpleEventBus;
import com.google.web.bindery.requestfactory.shared.RequestFactory;
import com.google.web.bindery.requestfactory.vm.RequestFactorySource;

public class IntegrationTestHelper {

  private static final String URL_TO_GWT_SERVLET = "http://localhost:8888/gwtRequest";

  /**
   * Creates a {@link RequestFactory}.
   */
  public static  T create( Class requestFactoryClass ) {
    T factory = RequestFactorySource.create( requestFactoryClass );
    factory.initialize( new SimpleEventBus(), new HttpPostTransport( URL_TO_GWT_SERVLET ) );
    return factory;
  }
}

HttpPostTransport is a new class that sends an http post request to the given url. Next to the payload, the send method receives a callback which is invoked to provide the result to the callee. Here’s the code:

package cleancodematters;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;

import com.google.web.bindery.requestfactory.shared.RequestFactory;
import com.google.web.bindery.requestfactory.shared.RequestTransport;
import com.google.web.bindery.requestfactory.shared.ServerFailure;

public class HttpPostTransport implements RequestTransport {

  private final String urlToGwtServlet;

  public HttpPostTransport( String urlToGwtServlet ) {
    this.urlToGwtServlet = urlToGwtServlet;
  }

  @Override
  public void send( String payload, TransportReceiver receiver ) {
    try {
      PostMethod post = createPostMethod( payload );
      int result = new HttpClient().executeMethod( post );
      handleResult( result, post, receiver );
      post.releaseConnection();
    } catch( Exception e ) {
      receiver.onTransportFailure( new ServerFailure( e.getMessage(), e.getClass().getName(), "",
          true ) );
    }
  }

  private PostMethod createPostMethod( String payload ) throws UnsupportedEncodingException {
    PostMethod post = new PostMethod( urlToGwtServlet );
    RequestEntity requestEntity = new StringRequestEntity( payload,
        RequestFactory.JSON_CONTENT_TYPE_UTF8, "UTF-8" );
    post.setRequestEntity( requestEntity );
    return post;
  }

  private void handleResult( int result, PostMethod post, TransportReceiver receiver )
      throws IOException {
    if( result == HttpStatus.SC_OK ) {
      receiver.onTransportSuccess( post.getResponseBodyAsString() );
    } else {
      receiver.onTransportFailure( new ServerFailure( "Server returned " + result ) );
    }
  }
}

With the help of these two classes, we can now start writing integration tests that run against a deployed system:

import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import org.junit.Test;
import org.mockito.ArgumentCaptor;

import com.google.web.bindery.requestfactory.shared.Receiver;

import cleancodematters.client.PizzaProxy;
import cleancodematters.client.PizzaRequestFactory;
import cleancodematters.client.PizzaRequestFactory.PizzaRequestContext;

@SuppressWarnings("unchecked")
public class PizzaIntegrationTest {

  @Test
  public void testSavePizza() {
    PizzaRequestFactory factory = IntegrationTestHelper.create( PizzaRequestFactory.class );

    PizzaRequestContext context = factory.context();
    PizzaProxy pizza = context.create( PizzaProxy.class );
    Receiver receiver = mock( Receiver.class );

    context.save( pizza ).fire( receiver );

    verify( receiver ).onSuccess( any( Void.class ) );
  }

  @Test
  public void testFindPizza() {
    PizzaRequestFactory factory = IntegrationTestHelper.create( PizzaRequestFactory.class );
    Receiver receiver = mock( Receiver.class );
    Long id = 1L;

    factory.context().findById( id ).fire( receiver );

    ArgumentCaptor captor = ArgumentCaptor.forClass( PizzaProxy.class );
    verify( receiver ).onSuccess( captor.capture() );
    PizzaProxy foundPizza = captor.getValue();
    assertEquals( id, foundPizza.getId() );
  }
}

That’s simple, isn’t it? But we can do even better! Although we are said to embrace asynchrony, the readability of our test would be greatly improved if we could get rid of the Receivers and ArgumentCaptors. This is possible now, because our new transport layer is synchronous.

Let’s add another method fire to the IntegrationTestHelper class:

package cleancodematters;

import com.google.web.bindery.requestfactory.shared.Receiver;
import com.google.web.bindery.requestfactory.shared.Request;
import com.google.web.bindery.requestfactory.shared.RequestFactory;
import com.google.web.bindery.requestfactory.shared.ServerFailure;

public class IntegrationTestHelper {

  private static class ReceiverCaptor extends Receiver {

    private T response;
    private ServerFailure serverFailure;

    @Override
    public void onSuccess( T response ) {
      this.response = response;
    }

    @Override
    public void onFailure( ServerFailure error ) {
      this.serverFailure = error;
    }

    public T getResponse() {
      return response;
    }

    public ServerFailure getServerFailure() {
      return serverFailure;
    }
  }

  /**
   * Allows firing a Request synchronously. In case of success, the result is returned.
   * Otherwise, a {@link RuntimeException} containing the server error message is thrown.
   */
  public static  T fire( Request request ) {
    ReceiverCaptor receiver = new ReceiverCaptor();

    request.fire( receiver );

    handleFailure( receiver.getServerFailure() );
    return receiver.getResponse();
  }

  private static void handleFailure( ServerFailure failure ) {
    if( failure != null ) {
      throw new RuntimeException( buildMessage( failure ) );
    }
  }

  private static String buildMessage( ServerFailure failure ) {
    StringBuilder result = new StringBuilder();
    result.append( "Server Error. Type: " );
    result.append( failure.getExceptionType() );
    result.append( " Message: " );
    result.append( failure.getMessage() );
    return result.toString();
  }
}

The fire method returns the return value on success or throws an RuntimeException in case of a failure. Now, the two tests above can be rewritten like this:

package cleancodematters;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.junit.Test;

import cleancodematters.client.PizzaProxy;
import cleancodematters.client.PizzaRequestFactory;
import cleancodematters.client.PizzaRequestFactory.PizzaRequestContext;

public class PizzaIntegrationTest {

  @Test
  public void testSavePizzaSmart() {
    PizzaRequestFactory factory = IntegrationTestHelper.create( PizzaRequestFactory.class );

    PizzaRequestContext context = factory.context();
    PizzaProxy pizza = context.create( PizzaProxy.class );

    IntegrationTestHelper.fire( context.save( pizza ) );
  }

  @Test
  public void testFindPizzaSmart() {
    PizzaRequestFactory factory = IntegrationTestHelper.create( PizzaRequestFactory.class );
    Long id = 1L;

    PizzaProxy foundPizza = IntegrationTestHelper.fire( factory.context().findById( id ) );

    assertEquals( id, foundPizza.getId() );
  }
}

Great. You can now even check for expected exceptions in a very natural way:

  @Test(expected = RuntimeException.class)
  public void testFindPizzaWithInvalidParameter() {
    PizzaRequestFactory factory = IntegrationTestHelper.create( PizzaRequestFactory.class );
    IntegrationTestHelper.fire( factory.context().findById( null ) );
  }
Advertisements

4 Responses to “Running integration tests against a GWT RequestFactory based back-end”

  1. Hello: Thanks for your GWT articles posted in your site, that’s quite helpful.

  2. This example ist great. It works like a charm.

Trackbacks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: