In the first part of the tutorial we set up EntityProxy classes for our back-end entities pizza and ingredient. A PizzaRequestContext was introduced that represents the client-side facade for the PizzaDao in the back-end.
Now, a natural next step is to write some kind of controller logic that uses the PizzaRequestContext to communicate with the back-end. Let’s call this controller PizzaManager:
package cleancodematters.client; import com.google.web.bindery.requestfactory.shared.Receiver; public class PizzaManager { private final PizzaRequestFactory factory; public PizzaManager( PizzaRequestFactory factory ) { this.factory = factory; } public void findById( Long id, Receiver<PizzaProxy> receiver ) { factory.context().findById( id ).with( "ingredients" ).fire( receiver ); } }
The manager gets a RequestFactory instance passed into the constructor. This is a good idea as creating the RequestFactory requires a GWT#create()
call which doesn’t work in plain JUnit tests. See my previous post on how to use GIN get the instance injected automatically.
How can we test the implementation of findById()
with plain JUnit tests? One approach is to use a mocked PizzaRequestFactory
instance. In our test we then have to ensure that the method chain factory.context().findById( id ).with( "ingredients" ).fire( receiver )
is called correctly. This test code is hard to write and also tied very closely with implementation details. In general, fluent interfaces are nice to read (but often violate the Law of Demeter, btw) but testing this code with mocks can be really cumbersome.
A better approach in my view is to use GWT’s RequestFactory infrastructure and replace the transport layer with some “in memory” processing that is independent of the browser infrastructure. Fortunately, GWT already provides a class for this: InProcessRequestTransport. This approach has another advantage: We also test the error-prone reference of nested entities (with( "ingredients" )
in the example).
Here’s the code to create an arbitrary RequestFactory to be used in tests:
public static <T extends RequestFactory> T create( Class<T> requestFactoryClass ) { ServiceLayer serviceLayer = ServiceLayer.create(); SimpleRequestProcessor processor = new SimpleRequestProcessor( serviceLayer ); T factory = RequestFactorySource.create( requestFactoryClass ); factory.initialize( new SimpleEventBus(), new InProcessRequestTransport( processor ) ); return factory; }
To write a complete test for findById()
another puzzle piece is missing: the back-end. Of course, we don’t want to invoke the real PizzaDao, which very likely makes calls to the database. Ideally, the PizzaDao is replaced with a mock, that can be instrumented to return some dummy data that we define in our test. We can then test the whole conversion to GWT EntityProxies and make sure all referenced entities are transferred, too (i.e. the with-clauses are correct).
To make this possible, we need to override the default lookup mechanism for the service locator and service instance. Instead of creating the PizzaDao, a mock should be returned. We also need to provide a method to allow tests to retrieve a the same mock instance. Here’s the helper class which accomplishes this:
package cleancodematters; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import java.util.HashMap; import java.util.Map; import org.mockito.ArgumentCaptor; import com.google.web.bindery.event.shared.SimpleEventBus; import com.google.web.bindery.requestfactory.server.ServiceLayer; import com.google.web.bindery.requestfactory.server.ServiceLayerDecorator; import com.google.web.bindery.requestfactory.server.SimpleRequestProcessor; import com.google.web.bindery.requestfactory.server.testing.InProcessRequestTransport; import com.google.web.bindery.requestfactory.shared.Receiver; import com.google.web.bindery.requestfactory.shared.RequestFactory; import com.google.web.bindery.requestfactory.shared.ServiceLocator; import com.google.web.bindery.requestfactory.vm.RequestFactorySource; @SuppressWarnings("unchecked") public class RequestFactoryHelper { private static class MockServiceLocator implements ServiceLocator { private final Map<Class<?>, Object> services = new HashMap<Class<?>, Object>(); @Override public Object getInstance( Class<?> clazz ) { // Make sure to return always the same mocked instance for each requested type Object result = services.get( clazz ); if (result == null) { result = mock( clazz ); services.put( clazz, result ); } return result; } } private static class MockServiceDecorator extends ServiceLayerDecorator { @Override public <T extends ServiceLocator> T createServiceLocator( Class<T> clazz ) { return (T) serviceLocator; } } private static MockServiceLocator serviceLocator = new MockServiceLocator(); private static ServiceLayer serviceLayer = ServiceLayer.create( new MockServiceDecorator() ); /** * Creates a {@link RequestFactory}. */ public static <T extends RequestFactory> T create( Class<T> requestFactoryClass ) { SimpleRequestProcessor processor = new SimpleRequestProcessor( serviceLayer ); T factory = RequestFactorySource.create( requestFactoryClass ); factory.initialize( new SimpleEventBus(), new InProcessRequestTransport( processor ) ); return factory; } /** * Returns the same service instance as used by the RequestFactory internals. */ public static <T> T getService( Class<T> serviceClass ) { T result = (T) serviceLocator.getInstance( serviceClass ); reset( result ); // reset mock to avoid side effects when used in multiple tests return result; } /** * Returns the value passed to {@link Receiver#onSuccess(Object)} */ public static <T> T captureResult( Receiver<T> receiver ) { ArgumentCaptor<Object> captor = ArgumentCaptor.forClass( Object.class ); verify( receiver ).onSuccess( (T) captor.capture() ); return (T) captor.getValue(); } }
This helper class can be used for any RequestFactory and any back-end service. I don’t like putting the MockingServiceLocator
into a static field. However this seems to be necessary, as the GWT ServiceLayer
uses an internal cache for service instances (ServiceLayerCache) which is static, too. To instrument the mocked service instances, the same instance must be returned in #getService(). That’s why, MockingServiceLocator adds mocks to a map and reuses them on subsequent calls.
Now, we can finally write a test to ensure findById()
works as expected:
package cleancodematters.client; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Collections; import java.util.List; import org.junit.Before; import org.junit.Test; import com.google.web.bindery.requestfactory.shared.Receiver; import cleancodematters.RequestFactoryHelper; import cleancodematters.server.PizzaDao; import cleancodematters.server.domain.Ingredient; import cleancodematters.server.domain.Pizza; @SuppressWarnings("unchecked") public class PizzaManagerTest { private PizzaRequestFactory factory; private PizzaDao dao; private PizzaManager manager; @Before public void setup() { factory = RequestFactoryHelper.create( PizzaRequestFactory.class ); dao = RequestFactoryHelper.getService( PizzaDao.class ); manager = new PizzaManager( factory ); } @Test public void testFindById() { // Create back-end entities String name = "Funghi"; Pizza expectedPizza = createPizza( name, Collections.singletonList( new Ingredient() ) ); Long id = Long.valueOf( 55 ); // Instrument mocked dao to return test entities when( dao.findById( id ) ).thenReturn( expectedPizza ); // call method that should be tested Receiver<PizzaProxy> receiver = mock( Receiver.class ); manager.findById( id, receiver ); // Get returned GWT entitiy proxy and compare values PizzaProxy returnedPizza = RequestFactoryHelper.captureResult( receiver ); assertEquals( name, returnedPizza.getName() ); assertEquals( 1, returnedPizza.getIngredients().size() ); } private static Pizza createPizza( String name, List<Ingredient> ingredients ) { Pizza expectedPizza = new Pizza(); expectedPizza.setName( name ); expectedPizza.setIngredients( ingredients ); return expectedPizza; } }
We can now test every aspect of the RequestFactory communication. If the with-clause was wrong or omitted, the test fails with a NPE as PizzaProxy#getIngredients()
returns null
. We can also throw an exception in the dao method and ensure that the correct handling is done on the client side.
It is remarkable that some twenty lines of test code are necessary to test a single line production code. However, that’s worth the price as client-server communication is crucial for most apps and should work as expected.
You can find the code in my github repository.