Google Code offered in: English - Español - 日本語 - 한국어 - Português - Pусский - 中文(简体) - 中文(繁體)
Writing unit tests that make use of the local service implementations that are bundled with the SDK is a natural and healthy thing to do. This chapter describes how to accomplish this task with JUnit 3. We'll go through the individual steps one by one and then tie them all together in a base class that your tests can extend.
The first thing you'll need to do is make sure you have appengine-api-stubs.jar
and appengine-local-runtime.jar
on the classpath for your unit test (these jars ship as part of the SDK). Next, we need to create an instance of ApiProxy.Environment
and register it with ApiProxy
. When your application is running locally the container creates this on your behalf using the contents of your appengine-web.xml
config file, but in this case JUnit is our container, and JUnit doesn't know anything at all about App Engine or appengine-web.xml
. So, it's up to the test to make sure an ApiProxy.Environment
is properly constructed and registered:
import com.google.apphosting.api.ApiProxy; class TestEnvironment implements ApiProxy.Environment { public String getAppId() { return "Unit Tests"; } public String getVersionId() { return "1.0"; } public void setDefaultNamespace(String s) { } public String getRequestNamespace() { return "gmail.com"; } public String getDefaultNamespace() { return ""; } public String getAuthDomain() { return "gmail.com"; } public boolean isLoggedIn() { return false; } public String getEmail() { return ""; } public boolean isAdmin() { return false; } } // ... ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment());
Once we've established an environment for the local service implementations to execute within, we need to set up the local service implementations themselves. The App Engine services that your application uses ultimately invoke ApiProxy.makeSyncCall()
, which in turn delegates to an ApiProxy
instance that, depending on whether you're running locally or in production, communicates with a local or a remote server-side implementation of the service. When you run your app locally, the container installs an implementation of ApiProxy
called ApiProxyLocalImpl
. In this case, however, our container is JUnit, and JUnit doesn't know that this needs to be set up. So, it's up to you to do it:
import java.io.File; import com.google.appengine.tools.development.ApiProxyLocalImpl; import com.google.apphosting.api.ApiProxy; ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")){});
And that's it!
We want to make it easy to write tests that make use of local service implementations, so let's wrap this up in a base TestCase that your tests can easily extend:
import com.google.appengine.tools.development.ApiProxyLocalImpl; import com.google.apphosting.api.ApiProxy; public class LocalServiceTestCase extends TestCase { @Override public void setUp() throws Exception { super.setUp(); ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment()); ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")){}); } @Override public void tearDown() throws Exception { // not strictly necessary to null these out but there's no harm either ApiProxy.setDelegate(null); ApiProxy.setEnvironmentForCurrentThread(null); super.tearDown(); } }
Now that we have a base TestCase it's simple to write tests that depend on local service implementations. Here's an example that tests that emails are properly sent when a bug gets created:
import com.google.appengine.api.mail.dev.LocalMailService; import com.google.appengine.tools.development.ApiProxyLocalImpl; public class BugNotificationTest extends TestCase { public void testEmailGetsSent() { ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate(); LocalMailService mailService = (LocalMailService) proxy.getService("mail"); mailService.clearSentMessages(); Bug b = new Bug(); b.setSeverity(Severity.LOW); b.setText("NullPointerException when trying to update phone number."); b.setOwner("max"); new BugDAO().createBug(b); assertEquals(1, mailService.getSentMessages().size()); // ... tests the content and recipient of the email } }
By default, the local implementation of the datastore service flushes its contents to disk at regular intervals. When you're running your application locally this is a nice feature because it maintains your state after you shut down the server. When you're running tests, however, it's much easier to write deterministic tests if every test starts with a clean datastore, and having state flushed to disk and then read back in can get in the way. Let's extend LocalServiceTestCase to demonstrate how to achieve this:
import com.google.appengine.api.datastore.dev.LocalDatastoreService; import com.google.appengine.tools.development.ApiProxyLocalImpl; public class LocalDatastoreTestCase extends LocalServiceTestCase { @Override public void setUp() throws Exception { super.setUp(); ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate(); proxy.setProperty(LocalDatastoreService.NO_STORAGE_PROPERTY, Boolean.TRUE.toString()); } @Override public void tearDown() throws Exception { ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate(); LocalDatastoreService datastoreService = (LocalDatastoreService) proxy.getService("datastore_v3"); datastoreService.clearProfiles(); super.tearDown(); } }
Now any test belonging to a TestCase that extends this class can be sure that it is starting with a clean datastore:
import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.Query; public class ContactInfoDAOTestCase extends LocalDatastoreTestCase { public void testBatchInsert() { ContactInfoDAO dao = new ContactInfoDAO(); ContactInfo info1 = new ContactInfo("John", "Doe", "650-555-5555"); ContactInfo info2 = new ContactInfo("Jane", "Doe", "415-555-5555"); dao.addContacts(info1, info2); Query query = new Query(ContactInfo.class.getSimpleName()); assertEquals(2, DatastoreServiceFactory.getDatastoreService().prepare(query).countEntities()); } public void testDelete() { ContactInfoDAO dao = new ContactInfoDAO(); ContactInfo info1 = new ContactInfo("John", "Doe", "650-555-5555"); dao.addContact(info1); dao.deleteContact(info1); Query query = new Query(ContactInfo.class.getSimpleName()); assertEquals(0, DatastoreServiceFactory.getDatastoreService().prepare(query).countEntities()); } }