My favorites | 中文(简体) | Sign in

使用本地服务实现进行单元测试

利用与 SDK 捆绑在一起的本地服务实现编写单元测试是自然且良好的操作。本章将描述如何使用 JUnit 3 完成此任务。我们将逐个介绍每一步骤,然后将所有这些步骤结合在一个基类中,您的测试可扩展该基类。

建立执行环境

您首先需要做的是确保您的单元测试的类路径上存在 appengine-api-stubs.jarappengine-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 实例。如果您在本地运行应用程序,则容器将安装名为 ApiProxyLocalImplApiProxy 实现.但是,在本例中,我们的容器是 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

为使编写利用本地服务实现的测试变得容易,我们将其包装在您的测试能够轻松扩展的基础 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());
    }
}