AppEngine用のアプリケーションのテストの手法について、公式ドキュメントがあまりにも貧弱なためそれを補足する資料を作ろうと思います。 DatastoreはもちろんMail送信やQueueへのTask投入等のテストを行う説明まで何回かに分けて徐々に書いていき、 最終的にはそれらのエントリを清書してWikiにまとめたいと思います。これはその第一回目で、テストのための仕組みの説明と基本的なテストの手順について説明します。
文中でApiProxy
と書かれているクラスはcom.google.apphosting.api.ApiProxy
の事です。
この説明での「AppEngine環境」とは、データストアサービスなどのAppEngineで提供されている各種サービス群を利用するための環境のことをさします。 テストのためにAppEngine環境を起動するには、大きくわけると
ApiProxy.setEnvironmentForCurrentThread()
ApiProxy.setDelegate()
ApiProxy.setEnvironmentForCurrentThread(ApiProxy.Environment)
AppEngineの実行環境ではスレッドごとにApiProxy.Environment
のインスタンスが必要となるので、
AppEngineの実行環境がApiProxy#getCurrentEnvironment()
を経由してApiProxy.Environment
のインスタンスを取得できるように設定する必要があります。
プロダクション環境や開発用のWebコンテナ経由で起動した場合はこれが自動的に設定されますが、それらを経由せず起動する場合は独自にインスタンスを作成・設定してやる必要があります。
ApiProxy.setDelegate(ApiProxy.Delegate<ApiProxy.Environment>)
AppEngineの実行環境では色々なサービスが存在しており、それらのサービスへのAPIが用意されています。
例えばデータストアサービスであればDatastoreServiceFactory#getDatastoreService()
を使うことでそれらのサービスへのAPIを使用することができます。
これら各サービスへのAPIは内部的にApiProxy#getDelegate()
で取得したApiProxy.Delegate
のインスタンスのmakeSyncCall()
というメソッドを通してサービスの実装へ処理を委譲します。
ApiProxy.Delegate
のインスタンスの中で各サービスの実装の解決や、それらとの通信が行われます。
この仕組みはプロダクション環境も開発環境でも同じ動作を行うように実装されており、それぞれの環境ごとにApiProxy#getDelegate()
が返すApiProxy.Delegate
の実装が違うだけ、となっています。
例えば開発用のWebコンテナ経由で起動された環境ではApiProxy#getDelegate()
はcom.google.appengine.tools.development.ApiProxyLocalImpl
というクラスのインスタンスが返されます。
ApiProxy.Environment
例えば以下のようなクラスを用意する事になります。
public class UnitTestEnvironment implements com.google.apphosting.api.ApiProxy.Environment { public String getAppId() { return "myApplicationId"; } public String getVersionId() { return "unittest"; } public String getRequestNamespace() { return ""; } public String getAuthDomain() { return "gmail.com"; } public boolean isLoggedIn() { return true; } public String getEmail() { return "unittest@gmail.com"; } public boolean isAdmin() { return true; } public java.util.Map<String, Object> getAttributes() { java.util.Map<String, Object> map = new java.util.HashMap<String, Object>(); return map; } }
この中で重要なメソッドについてピックアップして説明しておきます。
- String getAppId()
- Webコンテナ経由で起動した場合に
appengine-web.xml
から読み込まれるアプリケーションのIDの事ですが、 Datastoreサービスを使用した際に重要となります。
というのもDatastoreサービスで読み込み・書き込みで使用する時には必ずアプリケーションIDが使用されるからです。 Datastoreサービスでエンティティを保存すると必ずKey
がエンティティに保存されますが、Key
には必ずアプリケーション名が含まれます。 ですので開発環境でひとつのlocal_db.bin
を使っていたとしても、 環境を起動したときのApiProxy.Environment#getAppId()
で書き込んだ時とは別のアプリケーションIDを使ったりすると、 local_db.binに書き込んまれているはずのエンティティが読み込めなかったりします。 - boolean isLoggedIn(), String getEmail(), boolean isAdmin()
- UserServiceを使用した場合に、ログイン済み・ログインユーザのEmailアドレス・ログインユーザが管理者権限をもっているか、を制御する事ができます。
実行時に制御したいのであれば
ApiProxy.Environment
の実装ないで状態に応じた値を返すようにするか、ApiProxy.Environment
の実装を複数用意してApiProxy#setCurrentEnvironment()
で切り替えるなどすれば良いです。
ApiProxyLocalImpl
ApiProxy.Environment
とは違い、
ApiProxy.Delegate<ApiProxy.Environment>
の実装は既存のcom.google.appengine.tools.development.ApiProxyLocalImpl
を利用することになります。
このクラスはコンストラクタにjava.io.File
のインスタンスでAppEngine環境を起動するフォルダを指定します。
ここで指定したフォルダ配下にWEB-INF/appengine-generated
等のフォルダが生成されます。
ここで指定したフォルダ配下が上書きされる…という事は、Webコンテナ経由で起動した時のWEB-INF/appengine-generated/local_db.bin
ファイルも上書きされるという事です。
そのため単体テストで使用するときには、このフォルダはWebコンテナ経由で起動する時のwar
フォルダを指定したくない場合が多々あります。
その時に少し問題になるのがWEB-INF/datastore-indexes.xml
やWEB-INF/queue.xml
です。
ApiProxyLocalImpl
はコンストラクタで指定されたAppEngine環境を起動するフォルダ配下に何かを書き込むだけではなく、
そのフォルダ配下のWEB-INF/datastore-indexes.xml
やWEB-INF/queue.xml
を読み込もうとします。
ですので、テスト環境でもQueueを使ったりindexについて厳密に処理したい場合はApiProxyLocalImpl
のコンストラクタに渡すフォルダに、
それらのファイルをコピーしておく必要があります。
実際にテストを行う手順
ここでの説明は公式ドキュメントとほぼ同じです。
テスト用のライブラリを配置するフォルダを準備する
プロジェクト直下にlib.test
というtestライブラリ用のフォルダを作成します。フォルダ名は例ですので自由に決めると良いです。
プロダクト用のライブラリはwar/WEB-INF/lib
に配置してclasspathに追加しますが、war/WEB-INF/lib
はプロダクト環境にデプロイされてしまいます。
そのため、テスト用のライブラリはデプロイする必要が無いため別のフォルダを用意します。
テストに必要なライブラリを配置する
AppEngineのSDKを展開したフォルダを「${appengine-sdk}」として記述します。 SDK本体ではなくEclipseのPluginだけをインストールしている場合は、${appengine-sdk}は 「eclipseをインストールしたフォルダ/plugins/com.google.appengine.eclipse.sdkbundle.../appengine-java-sdk-xxx」 となります。「...」や「xxx」はSDKのバージョンやEclipsePluginのバージョンによって異なります。
以下のふたつのjarがテストに必要ですので、testライブラリ用のフォルダにコピーしclasspathに追加します。
- ${appengine-sdk}/lib/impl/appengine-api-stubs.jar
- ${appengine-sdk}/lib/impl/appengine-local-runtime.jar
また上記のappengine用のjarだけではなくJUnitのjarもtestライブラリ用のフォルダに配置してclasspathに追加しておきましょう。
ApiProxy.Environmentの実装を用意する
今回は先のApiProxy.Environment
に記述したUnitTestEnvironment
をそのまま使用することにします。
これは複数必要であれば複数の実装を用意しておき適宜切り替えを行えば良いです。
AppEngine環境を起動する
各テストの実行前に、かならずAppEngine環境を起動する処理が必要となります。
JUnit4では@Before
か@BeforeClass
で修飾されたメソッド内に実装すると良いです。
ApiProxy.Environement environment = new UnitTestEnvironment(); ApiProxyLocal apiProxyLocal = new ApiProxyLocalImpl(new File(testFolderName)) { }; ApiProxy.setEnvironmentForCurrentThread(environment); ApiProxy.setDelegate(apiProxyLocal);
サンプルコード中のtestFolderName
はテスト環境用のフォルダです。
bin
やtarget
等の、出力用フォルダを指定しておくのが良いです。
またApiProxyLocal#setProperty(String 設定キー, String 設定値)
を使って各サービスの実装の動作を設定することもできます。
例えばファイルを使わずにメモリ上だけでDatastoreを操作したい場合は以下のような処理になります。
ApiProxy.Environement environment = new UnitTestEnvironment(); ApiProxyLocal apiProxyLocal = new ApiProxyLocalImpl(new File(testFolderName)) { }; apiProxyLocal.setProperty(LocalDatastoreService.NO_STORAGE_PROPERTY, Boolean.TRUE.toString()); ApiProxy.setEnvironmentForCurrentThread(environment); ApiProxy.setDelegate(apiProxyLocal);
ファイルを使って処理するが、データを全て削除したい場合には以下の処理を実行します。
LocalDatastoreService datastoreService = (LocalDatastoreService) ((ApiProxyLocal) ApiProxy.getDelegate()).getService(LocalDatastoreService.PACKAGE); datastoreService.clearProfiles();
QueueにTaskを投入したりdatastore-indexes.xmlの定義を必要とする場合はそれらをテスト環境用のフォルダにコピーしておく必要があります。
詳しくは先のApiProxy.Delegate
の説明に書いた内容を参考にしてください。
AppEngine環境を終了する
各テストやテストケースの実行後に、起動した環境を終了する処理が必要となります。
JUnit4なら、起動処理を@Before
か@BeforeClass
で行っているでしょうから、
それに対応する@After
や@AfterClass
で修飾されたメソッド内に実装することになります。
ApiProxy.setDelegate(null); ApiProxy.setEnvironmentForCurrentThread(null);
基本的には上記の処理だけで良いですが、local_db.bin
がクローズされるタイミング等の問題で
上記の処理だけではなく以下のようにしておいた方が良い場合もあるかもしれません。
if (ApiProxy.getDelegate() != null) { ((ApiProxyLocal) ApiProxy.getDelegate()).stop(); } ApiProxy.setDelegate(null); ApiProxy.setEnvironmentForCurrentThread(null);
目的のテストを記述し、実行する
上記の準備を整えるだけで単純なテストは既に実行できる状態になっています。以下はその全体の例です。
import java.io.File; import java.util.*; import org.junit.*; import com.google.appengine.api.datastore.*; import com.google.appengine.api.datastore.dev.LocalDatastoreService; import com.google.appengine.tools.development.*; import com.google.apphosting.api.ApiProxy; import com.google.apphosting.api.ApiProxy.Environment; import static org.junit.Assert.assertEquals; public class DatastoreTest { @Test public void datastoreTest01() { Entity parent = new Entity("child"); parent.setProperty("prop1", "fuga"); DatastoreService service = DatastoreServiceFactory.getDatastoreService(); service.put(parent); int size = service.prepare(new Query("child")).asList(FetchOptions.Builder.withOffset(0)).size(); assertEquals(1, size); } @Test public void datastoreTest02() { DatastoreService service = DatastoreServiceFactory.getDatastoreService(); Key parentKey = service.allocateIds("parent", 1).getStart(); Iterator<Key> childKeys = service.allocateIds(parentKey, "child", 2).iterator(); Key childKey1 = childKeys.next(); Key childKey2 = childKeys.next(); Entity parent = new Entity(parentKey); parent.setProperty("prop1", "hoge"); Entity child1 = new Entity(childKey1); child1.setProperty("prop1", "foo"); Entity child2 = new Entity(childKey2); child2.setProperty("prop1", "bar"); service.put(Arrays.asList(parent, child1, child2)); int size = service.prepare(new Query("child")).asList(FetchOptions.Builder.withOffset(0)).size(); assertEquals(2, size); } @Before public void setUp() { ApiProxyLocal apiProxyLocal = new ApiProxyLocalImpl(new File("bin/datastoreTest")) {}; Environment environment = new UnitTestEnvironment(); ApiProxy.setEnvironmentForCurrentThread(environment); ApiProxy.setDelegate(apiProxyLocal); } @After public void tearDown() { ApiProxyLocal apiProxyLocal = (ApiProxyLocal) ApiProxy.getDelegate(); if (apiProxyLocal != null) { LocalDatastoreService datastoreService = (LocalDatastoreService) apiProxyLocal.getService(LocalDatastoreService.PACKAGE); datastoreService.clearProfiles(); apiProxyLocal.stop(); } ApiProxy.setDelegate(null); ApiProxy.setEnvironmentForCurrentThread(null); } }
おまけ: 開発環境での各サービスの実装クラス
特に使用する機会は無いと思いますが、開発環境でサービスの実装の振る舞いを変更する設定値だとかを知りたいときなどに役に立つかもしれません。全てのクラスがcom.google.appengine.tools.development.LocalRpcService
を実装しており、
${appengine-sdk}/lib/impl/appengine-api-stubs.jar
に含まれています。
一覧の各サービス名にくっついているカッコ内太字で表記されている文字列はApiProxy.Delegate#makeSyncCall()
への引数として渡されるサービスの内部的な名称です。
これは開発環境ではcom.google.appengine.tools.development.LocalRpcService
でString getPackage()
として定義されています。
- BlobstoreService(blobstore)
- com.google.appengine.api.blobstore.dev.LocalBlobstoreService
- DatastoreService(datastore_v3)
- com.google.appengine.api.datastore.dev.LocalDatastoreService
- ImagesService(images)
- com.google.appengine.api.images.dev.LocalImagesService
- MailService(mail)
- com.google.appengine.api.mail.dev.LocalMailService
- MemcacheService(memcache)
- com.google.appengine.api.memcache.dev.LocalMemcacheService
- TaskQueue(taskqueue)
- com.google.appengine.api.labs.taskqueue.dev.LocalTaskQueue
- URLFetchService(urlfetch)
- com.google.appengine.api.urlfetch.dev.LocalURLFetchService
- UserService(user)
- com.google.appengine.api.users.dev.LocalUserService
- XmppService(xmpp)
- com.google.appengine.api.xmpp.dev.LocalXmppService
追記
@mokkouyouさんが誤字を指摘してくれましたので、修正しました。
0 件のコメント:
コメントを投稿