利用与 SDK 捆绑在一起的本地服务实现编写单元测试是自然且良好的操作。本章将描述如何使用 JUnit 3 完成此任务。我们将逐个介绍每一步骤,然后将所有这些步骤结合在一个基类中,您的测试可扩展该基类。
您首先需要做的是确保您的单元测试的类路径上存在 appengine-api-stubs.jar 和 appengine-local-runtime.jar(这些 jar 作为 SDK 的一部分发行)。接下来,您需要创建一个 ApiProxy.Environment 实例并使用 ApiProxy 注册该实例。当您的应用程序在本地运行时,容器将使用 appengine-web.xml 配置文件中的内容为您创建该实例,但在本例中,我们的容器是 JUnit,且 JUnit 对 App Engine 或 appengine-web.xml 一无所知。因此,应由测试负责确保 ApiProxy.Environment 已正确构造并注册。
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 null;
}
public String getDefaultNamespace() {
return null;
}
public String getAuthDomain() {
return null;
}
public boolean isLoggedIn() {
return false;
}
public String getEmail() {
return null;
}
public boolean isAdmin() {
return false;
}
}
// ...
ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment());
在我们建立了将在其中执行本地服务实现的环境后,还需要对本地服务实现本身进行设置。您的应用程序最终使用的 App Engine 服务将调用 ApiProxy.makeSyncCall(),该方法反过来将委派与本地或远程(取决于您是在本地运行还是在生产中运行)服务器端的服务通信的 ApiProxy 实例。如果您在本地运行应用程序,则容器将安装名为 ApiProxyLocalImpl 的 ApiProxy 实现.但是,在本例中,我们的容器是 JUnit,而 JUnit 并不知道这需要进行设置。因此,需要您来执行该操作:
import java.io.File;
import com.google.appengine.tools.development.ApiProxyLocalImpl;
import com.google.apphosting.api.ApiProxy;
ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")){});
就这些!
为使编写利用本地服务实现的测试变得容易,我们将其包装在您的测试能够轻松扩展的基础 TestCase 中:
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();
}
}
有了基础 TestCase,编写基于本地服务实现的测试将会很简单。以下是一个测试错误发生时电子邮件是否正常发送的示例:
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
}
}
默认情况下,数据存储区服务的本地实现会按固定间隔将其内容刷新到磁盘。如果您是在本地运行应用程序,那么该功能很有用,因为它将在您关闭服务器之后保存您的状态。但是在运行测试时,如果每个测试都从干净的数据存储区开始,则编写确定性测试将更容易,将状态刷新至磁盘然后读回可能会产生妨碍。我们将扩展 LocalServiceTestCase 来演示如何完成该操作:
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();
}
}
现在,所有属于扩展此类的 TestCase 的测试都可确保从干净的数据存储区开始:
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());
}
}