Here’s an easy recipe for running server-side unit tests against your ActionHandlers. Thanks to Guice, you can simply replace the real dispatch servlet with a DispatchTestService that you call from your unit tests. You’ll pass it an Action and get back a Result just the same as you would from the client. Here’s a simple TestCase (note: I’m extending the BaseTest from AppEngineFan that I mentioned in my previous post in order to get access to a test AppEngine environment).
package com.roa.test; import net.customware.gwt.dispatch.client.standard.StandardDispatchService; import com.appenginefan.toolkit.unittests.BaseTest; import com.google.inject.Guice; import com.google.inject.Injector; import com.roa.client.domain.User; import com.roa.server.guice.ServerModule; import com.roa.shared.rpc.AddUserAction; import com.roa.shared.rpc.AddUserResult; public class AddUserTestCase extends BaseTest { private StandardDispatchService testSvc; @Override protected void setUp() throws Exception { super.setUp(); Injector inj = Guice.createInjector(new ServerModule(), new DispatchTestModule()); testSvc = inj.getInstance(StandardDispatchService.class); } public void testAddUser() throws Exception { // Create new user User u = new User(); u.setEmailAddress("test@example.com"); u.setFirstName("Test"); u.setLastName("User"); u.setGoogleAccountId("testAccountId"); AddUserResult userResult = (AddUserResult) testSvc.execute( new AddUserAction(u)); u = userResult.getUser(); } }
The Guice injector in the setUp method uses our real ServerModule, which maps each Action to its ActionHandler. The DispatchTestModule simply binds the gwt-dispatch StandardDispatchService to a test implementation. Here’s the DispatchTestModule:
package com.roa.test; import net.customware.gwt.dispatch.client.standard.StandardDispatchService; import com.google.inject.AbstractModule; import com.google.inject.Singleton; public class DispatchTestModule extends AbstractModule { @Override protected void configure() { bind(StandardDispatchService.class).to(DispatchTestService.class).in( Singleton.class); } }
And our DispatchTestService:
package com.roa.test; import net.customware.gwt.dispatch.client.standard.StandardDispatchService; import net.customware.gwt.dispatch.server.Dispatch; import net.customware.gwt.dispatch.shared.Action; import net.customware.gwt.dispatch.shared.Result; import com.google.inject.Inject; public class DispatchTestService implements StandardDispatchService { private Dispatch dispatch; @Inject public DispatchTestService(Dispatch dispatch) { this.dispatch = dispatch; } @Override public Result execute(Action<?> action) throws Exception { Result result = dispatch.execute(action); return result; } }
Several of my ActionHandlers call the AppEngine UserService, so I’ve modified AppEngineFan’s TestEnvironment class to supply test values:
... public String getEmail() { // throw new UnsupportedOperationException(); return "test@example.com"; } public boolean isLoggedIn() { // throw new UnsupportedOperationException(); return true; } public String getAuthDomain() { // throw new UnsupportedOperationException(); return "test"; } ...
I’m using the StandardDispatchService in unit tests in place of the AppEngineDispatchService I wrote about in a previous post because there’s no point in passing the extra session ID parameter with each call to dispatch.execute(). This is yet another benefit to using gwt-dispatch: all my service logic resides in ActionHandlers and can therefore be tested without so much as a mock servlet.
To get to the AppEngine Datastore, my ActionHandlers are using a PersistenceManagerFactory singleton that calls JDOHelper.getPersistenceManagerFactory(). Thankfully, this seems to work just fine in the tests, and I haven’t (yet?) had a need to inject different instances of a PersistenceManager in test vs. main code. See the previous post for a reference to the AppEngineFan code that initializes the AppEngine test environment, including Datastore. Note that the AppEngineFan TestInitializer sets the Datastore service NO_STORAGE_PROPERTY to true. This means that each test method in your TestCase starts with an empty Datastore, so each method needs to begin by populating the data it needs for the test.