地球ウォーカー2

Scala, Python の勉強日記

GAEをJUnitで単体テストするときにはまった3つの罠

はまりまくって貴重な祝日が台無しになったので憂さ晴らしエントリ。
やろうとしたことは、GAEのURLFetchServiceを使って取得したHTTPResponseをいじくりまわすロジックのテスト。

罠1:そのまま実行できない

何も考えずにJUnitでテストコードを書いて実行するとExceptionが発生する。

The API package 'urlfetch' or call 'Fetch()' was not found.

実行したのは以下のようなテストコード。デバッグしてみると、service.fetch(url)の部分で例外が発生している模様。

import static org.junit.Assert.fail;
import java.net.URL;
import org.junit.Test;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;

public class UrlFetchServiceTest {

    @Test
    public void test() {
        doTest();
    }

    private void doTest() {
        try {
            URL url = new URL("http://feeds.feedburner.com/hatena/b/hotentry");
            URLFetchService service = URLFetchServiceFactory.getURLFetchService();
            HTTPResponse response = service.fetch(url); // ← ここで例外が発生してしまう

            // responseをいじくりまわすメソッドを呼び出す

        } catch (Exception e) {
            fail(e.getMessage());
        }
    }
}

考えてもわからないのでとりあえずGoogleの公式ドキュメント(日本語版)を見たところ、ローカルの実行環境構築にはApiProxy.EnvironmentimplementsしたTestEnvironmentクラスを作成した上でApiProxyに設定すれば良いらしい。

ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment());
ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")){});

見よう見まねでTestEnvironmentクラスを作った。ここまではうまくいった。
ところが、ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")){});を記述しようと思ったら・・・。

罠2:日本版のドキュメントが古い*1

ApiProxyLocalImplクラスのコンストラクタが呼び出せない><
どうやらSDKのバージョンアップでApiProxyLocalImplコンストラクタがpublicprivateに変わったらしい*2
途方に暮れつつググってたら、英語版のドキュメントを発見。曰く、

The most important class in this package is com.google.appengine.tools.development.testing.LocalServiceTestHelper, which handles all of the necessary environment setup and gives you a top-level point of configuration for all the local services you might want to access in your tests. In order to write a test that accesses a specific local service, create an instance of LocalServiceTestHelper with a com.google.appengine.tools.development.testing.LocalServiceTestConfig implementation for that specific local service, then call setUp() on your LocalServiceTestHelper instance before each test and tearDown() after each test.

つまり、テストコード内でLocalServiceTestHelperを使いたいサービスでもってインスタンス化し、テスト前とテスト後にそれぞれhelper#setUphelper#tearDownを呼び出せと。
これを踏まえて前のコードを変更すると、

import static org.junit.Assert.fail;

import java.net.URL;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig;

public class UrlFetchServiceTest {
    // UrlFetchServiceTestを使うのでLocalURLFetchServiceTestConfigでインスタンス化する
    private final LocalServiceTestHelper helper =
        new LocalServiceTestHelper(new LocalURLFetchServiceTestConfig());

    @Before
    public void setUp() {
        helper.setUp();
    }

    @After
    public void tearDown() {
        helper.setUp();
    }

    @Test
    public void test() {
        doTest();
    }
    // .... 以下は前のコードと同じ
}

日本語版で書いてあった古いバージョンの方法よりかなりシンプルになっている。
コンパイルに必要なappengine-testing.jarMavenリポジトリにインストールされていれば

<dependency>
    <groupId>com.google.appengine</groupId>
    <artifactId>appengine-testing</artifactId>
    <version>1.3.7</version>
    <type>jar</type>
    <scope>test</scope>
</dependency>

を記述するだけで追加できる。まだインストールしていない場合はこのサイトで入手できる。

で、いざ実行!

The API package 'urlfetch' or call 'Fetch()' was not found.

・・・同じエラーが出た。

3.ランタイムライブラリが必要

まぁこれはドキュメントに書いてあるのを見逃していただけなんだけど、ランタイムライブラリにappengine-api-stubs.jar*3が必要らしい。

<dependency>
    <groupId>com.google.appengine</groupId>
    <artifactId>appengine-api-stubs</artifactId>
    <version>1.3.7</version>
    <type>jar</type>
    <scope>test</scope>
</dependency>

ライブラリを追加したら無事動いた。

感想

解決して良かった。そしてこれからは英語ドキュメントを先に読もう、と心に誓った。

その他メモ

  • LocalServiceTestHelperのコンストラクタは可変引数になっているため、複数サービスを含むテストをするときはまとめてインスタンス化できる。
// 例)Webサイトをスクレイピングしてデータベースに格納するロジックのテストを行う。
private final LocalServiceTestHelper helper
    = new LocalServiceTestHelper(new LocalURLFetchServiceTestConfig(),
                                 new LocalDatastoreServiceTestConfig()
                                 );
  • LocalServiceTestHelperの引数に指定可能なTestConfigクラスは以下があることを確認。
    • LocalBlobstoreServiceTest
    • LocalCapabilitiesServiceTest
    • LocalChannelServiceTest
    • LocalDatastoreServiceTest
    • LocalImagesServiceTest
    • LocalMailServiceTest
    • LocalMemcacheServiceTest
    • LocalTaskQueueTest
    • LocalURLFetchServiceTest
    • LocalUserServiceTest
    • LocalXMPPServiceTest

*1:2010/09/21現在

*2:どのタイミングで変わったのかはわからないが、少なくとも1.3.7ではprivateになっている

*3:使用するサービスによってはappengine-api-labs.jarも必要かも