Избранное | Русский | Войти

Поузловая проверка, использующая локальные реализации служб

Подготавливать тесты для поузловой проверки, использующие локальные реализации служб, поставляемые вместе с SDK, в порядке вещей. В этой главе описано, как это сделать с помощью JUnit3. Мы рассмотрим все этапы по порядку, а затем объединим их в базовом классе, который можно расширить своими тестами.

Установка среды выполнения

В первую очередь, нужно удостовериться, что appengine-api-stubs.jar и appengine-local-runtime.jar находятся там же, где и тесты для поузловой проверки (эти JAR поставляются вместе с SDK). Затем нужно создать экземпляр класса ApiProxy.Environment и зарегистрировать его с помощью ApiProxy. Если приложение выполняется локально, контейнер создает его автоматически, используя содержание файла конфигурации appengine-web.xml. Однако, в нашем случае контейнером является 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, который в зависимости от того, выполняется ли приложение локально или находится в работе на сервере, взаимодействует с локальной или удаленной реализацией службы. При выполнении приложения локально контейнер устанавливает реализацию класса ApiProxy, вызывая ApiProxyLocalImpl. Однако, в этом случае контейнер представляет 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();
    }
}

Теперь, когда у нас есть базовый класс, можно с легкостью писать тесты, зависящие от локальных реализаций служб. Вот пример тестов, которые проверяют, что при создании записи об ошибке электронные сообщения отправляются правильно:

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());
    }
}