お気に入り | 日本語 | ログイン

ローカル サービスの実装でのユニットのテスト

SDK にバンドルされているローカル サービスの実装を活用したユニット テストの記述は、ごく通常の手順であり、効果的です。この章では、JUnit 3 を使用したテストの実行について説明します。それぞれの手順を個別に説明した後に、基本クラスにまとめます。このクラスを拡張し独自のテストを作成できます。

実行環境の構築

最初に、ユニット テストの classpath に 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 インスタンスの代替となります。このインスタンスは、ローカルまたは本番環境のどちらで実行しているかにより、ローカルまたはサービスのリモート サーバー側の実装にアクセスします。アプリケーションをローカルで実行している場合、コンテナは ApiProxy の実装である ApiProxyLocalImpl をインストールします。しかしこの場合、使用するコンテナは 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());
    }
}