App Engineのユニットテストにて任意の例外発生を試す

タスクチェインの実装をしていると、テスト環境でもDEE発生をシミュレーションしたくなるのが人情というものです。
shin1ogawaさんのエントリでは下記のように説明されています。

テストのためにAppEngine環境を起動するには、大きくわけると

ApiProxy.setEnvironmentForCurrentThread()
ApiProxy.setDelegate()
のふたつの処理が必要になります。
ApiProxy.setEnvironmentForCurrentThread(ApiProxy.Environment)

AppEngineの実行環境ではスレッドごとにApiProxy.Environmentのインスタンスが必要となるので、 AppEngineの実行環境がApiProxy#getCurrentEnvironment()を経由してApiProxy.Environmentのインスタンスを取得できるように設定する必要があります。 プロダクション環境や開発用のWebコンテナ経由で起動した場合はこれが自動的に設定されますが、それらを経由せず起動する場合は独自にインスタンスを作成・設定してやる必要があります。
404 shin1のつぶやき ないわー Not Found: AppEngine用のアプリケーションの自動テストについて(1)

Slim3では、このあたりのことをAppEngineTesterがやってくれています。

AppEngineTesterが無いとどうなるのか試す

AppEngineTesterの役目を知るには、AppEngineTesterを使わないでみるのが一番です。
quickstartのSlim3Serviceのテストケースから、AppEngineTesterをコメントアウトします。

package slim3sandbox.service;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slim3.tester.AppEngineTester;

public class Slim3ServiceTest {

	@Test
	public void slim3test1() {
		Slim3Service.newAndPut("abc");
		assertThat(Slim3Service.queryAll().size(), is(equalTo(1)));
	}

	private static AppEngineTester testHelper;

	@Before
	public void setUp() throws Exception {
		//testHelper = new AppEngineTester();
		//testHelper.setUp();
	}

	@After
	public void tearDown() throws Exception {
		//testHelper.tearDown();
	}
}

上記を実行すると、下記のトレース出力。

java.lang.NullPointerException: No API environment is registered for this thread.
at com.google.appengine.api.datastore.DatastoreApiHelper.getCurrentAppId(DatastoreApiHelper.java:74)
at com.google.appengine.api.datastore.DatastoreApiHelper.getCurrentAppIdNamespace(DatastoreApiHelper.java:84)
at com.google.appengine.api.datastore.Key.(Key.java:104)
at com.google.appengine.api.datastore.Key.(Key.java:92)
at com.google.appengine.api.datastore.KeyFactory.createKey(KeyFactory.java:72)
at com.google.appengine.api.datastore.DatastoreServiceImpl.buildAllocateIdsRef(DatastoreServiceImpl.java:414)
at com.google.appengine.api.datastore.DatastoreServiceImpl.allocateIds(DatastoreServiceImpl.java:400)
at com.google.appengine.api.datastore.DatastoreServiceImpl.allocateIds(DatastoreServiceImpl.java:387)
at org.slim3.datastore.DatastoreUtil.allocateIds(DatastoreUtil.java:283)
at org.slim3.datastore.DatastoreUtil.allocateId(DatastoreUtil.java:229)
at org.slim3.datastore.Datastore.allocateId(Datastore.java:138)
at org.slim3.datastore.ModelMeta.assignKeyIfNecessary(ModelMeta.java:276)
at slim3sandbox.meta.Slim3ModelMeta.prePut(Slim3ModelMeta.java:78)
at org.slim3.datastore.DatastoreUtil.modelToEntity(DatastoreUtil.java:1642)
at org.slim3.datastore.Datastore.put(Datastore.java:2195)
at slim3sandbox.service.Slim3Service.newAndPut(Slim3Service.java:19)
at slim3sandbox.service.Slim3ServiceTest.slim3test1(Slim3ServiceTest.java:15)
(略)

Environmentがないよ、ということです。空実装のApiProxy.Environmentをセットして、再度実行します。

	@Before
	public void setUp() throws Exception {
		ApiProxy.setEnvironmentForCurrentThread(new ApiProxy.Environment() {
		
			@Override
			public Map<String, Object> getAttributes() {
				// TODO Auto-generated method stub
				return Maps.newHashMap();
			}
			
			@Override
			public String getAppId() {
				// TODO Auto-generated method stub
				return "";
			}
                        //以下略
		});

com.google.apphosting.api.ApiProxy$CallNotFoundException: The API package 'datastore_v3' or call 'AllocateIds()' was not found.
at com.google.apphosting.api.ApiProxy.makeSyncCall(ApiProxy.java:95)
at com.google.appengine.api.datastore.DatastoreApiHelper.makeSyncCall(DatastoreApiHelper.java:58)
at com.google.appengine.api.datastore.DatastoreServiceImpl.allocateIds(DatastoreServiceImpl.java:404)
at com.google.appengine.api.datastore.DatastoreServiceImpl.allocateIds(DatastoreServiceImpl.java:387)
at org.slim3.datastore.DatastoreUtil.allocateIds(DatastoreUtil.java:283)
at org.slim3.datastore.DatastoreUtil.allocateId(DatastoreUtil.java:229)
at org.slim3.datastore.Datastore.allocateId(Datastore.java:138)
at org.slim3.datastore.ModelMeta.assignKeyIfNecessary(ModelMeta.java:276)
at slim3sandbox.meta.Slim3ModelMeta.prePut(Slim3ModelMeta.java:78)
at org.slim3.datastore.DatastoreUtil.modelToEntity(DatastoreUtil.java:1642)
at org.slim3.datastore.Datastore.put(Datastore.java:2195)
at slim3sandbox.service.Slim3Service.newAndPut(Slim3Service.java:19)
at slim3sandbox.service.Slim3ServiceTest.slim3test1(Slim3ServiceTest.java:20)
(略)

makeSyncCallを呼び出してるっぽいです。ApiProxyに、先ほどのEnvironmentに加え空実装のDelegateをセットしてみます。

		ApiProxy.setDelegate(new ApiProxy.Delegate<Environment>() {
			@Override
			public void log(Environment arg0, LogRecord arg1) {
				// TODO Auto-generated method stub
				
			}

			@Override
			public Future<byte[]> makeAsyncCall(Environment arg0, String arg1, String arg2, byte[] arg3, ApiConfig arg4) {
				// TODO Auto-generated method stub
				return null;
			}

			@Override
			public byte[] makeSyncCall(Environment arg0, String arg1, String arg2, byte[] arg3)
					throws ApiProxyException {
				// TODO Auto-generated method stub
				return null;
			}
		});

上記を実行すると、グリーンになりました。
上記を踏まえてAppEngineTesterの中身を見てみます。AppEngineTesterは、Delegateをimplementしています。
makeSyncCallをオーバーライドすれば、任意の例外を発生させたりできそうです。
下記のようなコードを書いてみます。

package slim3sandbox.service;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slim3.tester.AppEngineTester;

import com.google.apphosting.api.DeadlineExceededException;
import com.google.apphosting.api.ApiProxy.ApiProxyException;
import com.google.apphosting.api.ApiProxy.Environment;

public class Slim3ServiceTest {

	@Test(expected=DeadlineExceededException.class)
	public void slim3test1() {
		Slim3Service.newAndPut("abc");
	}

	private static AppEngineTester testHelper;

	@Before
	public void setUp() throws Exception {
		testHelper = new AppEngineTester() {
			@Override
			public byte[] makeSyncCall(Environment env, String service, String method, byte[] requestBuf)
					throws ApiProxyException {
				throw new DeadlineExceededException();
			}
		};
		testHelper.setUp();
		
	}

	@After
	public void tearDown() throws Exception {
		testHelper.tearDown();
		
	}
}

グリーンになりました。
とりあえず、任意の例外を発生させるテストケースが書けるようになりました。