2010年1月6日水曜日

#AppEngine 用のアプリケーションの自動テストについて(4) - TaskQueueに関するテスト

下記のAppEngineアプリケーションの自動テストシリーズに続く、第四回目です。

Taskの「投入」をテストする

TaskQueueのテストといっても「Taskの投入」をテストするのか「Taskの実行」をテストするのか、といった2種類のテスト対象が考えられます。 今回は「Taskの投入」をテストする説明をします。「WebHandlerによるTask実行」については、例えば私はテストしやすいように以下のような手法で実装しています。

  • 何らかの機能で、Taskを投入する
  • 投入されたTaskはWebHandler内で実行することになるが、WebHandler内にはほとんど処理を書かず、 int HogeTask.execute(Map<String, String> parameters)という、リクエストパラメータを渡してHttpStatusを返すようなメソッドに処理を委譲する

このようにしておく事で、「Taskの実行」は通常の処理と同じように簡単にテストできる事が多いです。

今回も前回と同じようにサービスへのリクエストをフックする ApiProxy.Delegateを作成し、ApiProxy#setDelegate()でそれを適用する手法でテストします。

Taskの投入をテストするためのApiProxy.Delegateの実装

例えばこのようなApiProxy.Delegateの実装を見てみましょう。 前回とほぼ同じ実装で、サービスへリクエストされたバイト配列を組み立てるオブジェクトが com.google.appengine.api.labs.taskqueue.TaskQueuePb.TaskQueueAddRequestに変わっている程度です。

import java.util.*;
import java.util.concurrent.Future;
import com.google.appengine.api.labs.taskqueue.TaskQueuePb.TaskQueueAddRequest;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.*;

public class TaskQueueDelegate implements ApiProxy.Delegate<Environment> {
  @SuppressWarnings("unchecked")
  public final ApiProxy.Delegate<Environment> apiProxyLocal = ApiProxy.getDelegate();
  
  public final List<TaskQueueAddRequest> tasks = new ArrayList<TaskQueueAddRequest>();
  
  public byte[] makeSyncCall(Environment env, String service, String method,
      byte[] request) throws ApiProxyException {
    if (service.equals("taskqueue") && method.equals("Add")) {
      try {
        TaskQueueAddRequest taskPb = new TaskQueueAddRequest();
        taskPb.mergeFrom(request);
        tasks.add(taskPb);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
    return apiProxyLocal.makeSyncCall(env, service, method, request);
  }

  @Override
  public Future<byte[]> makeAsyncCall(Environment env, String service,
      String method, byte[] request, ApiConfig config) {
    return apiProxyLocal.makeAsyncCall(env, service, method, request, config);
  }

  @Override
  public void log(Environment env, LogRecord logRecord) {
    apiProxyLocal.log(env, logRecord);
  }
}

前回と同様にサービスへリクエストするバイト配列から ProtocolBufferオブジェクトを組み立てて、テストケースから評価できるようにそれを保持しているだけです。

TaskQueueサービスを起動するための準備

以下の二つの手順が必要になります。

  1. ApiProxy#setCurrentEnvironment()に設定するApiProxy.Environmentで実装するメソッドに Map<String, String> ApiProxy.Environment#getAttribute()があるが、 これが返すMapオブジェクトは"com.google.appengine.server_url_key"というキーに何か値が設定されている必要がある。
  2. デフォルトのキュー以外のキューを使用するのであれば、war/WEB-INF/queue.xmlをテスト用のフォルダにコピーしておく必要がある。

最初の手順については、第一回目の説明で UnitTestEnvironmentというクラス名で説明していたApiProxy.EnvironmentMap<String, String> ApiProxy.Environment#getAttribute()メソッドの実装を修正する必要があります。

public java.util.Map<String, Object> getAttributes() {
  java.util.Map<String, Object> map = new java.util.HashMap<String, Object>();
  map.put("com.google.appengine.server_url_key", "dummy");
  return map;
}

テストケースの例

このApiProxy.Delegateの実装を使ったテストケースの例は以下のようになります。 デフォルトキューにTaskを追加する処理と、名前付きのキューにTaskを追加する処理のふたつをテストしています。 先に説明したとおりにUnitTestEnvironment#getAttribute()メソッドの修正が行われていることが前提です。

また、ファイルのコピーを簡単にするために、commons-ioを利用しています。

import java.io.*;
import java.net.URLEncoder;
import org.apache.commons.io.FileUtils;
import org.junit.*;
import com.google.appengine.api.labs.taskqueue.*;
import com.google.appengine.api.labs.taskqueue.TaskQueuePb.*;
import com.google.appengine.tools.development.*;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.*;
import static org.junit.Assert.assertEquals;

public class TaskQueueTest {

  @Test public void addTaskToDefaultQueue() throws UnsupportedEncodingException {
    TaskQueueDelegate delegate = new TaskQueueDelegate();
    ApiProxy.setDelegate(delegate);
    
    Queue queue = QueueFactory.getDefaultQueue();
    queue.add(TaskOptions.Builder.url("/tqHandler").param("key", "あいうえお"));

    assertEquals(1, delegate.tasks.size());
    TaskQueueAddRequest task = delegate.tasks.get(0);
    assertEquals("/tqHandler", task.getUrl());
    assertEquals("key="+URLEncoder.encode("あいうえお", "utf-8"), task.getBody());
  }

  @Test public void addTaskToNamedQueue() throws UnsupportedEncodingException {
    TaskQueueDelegate delegate = new TaskQueueDelegate();
    ApiProxy.setDelegate(delegate);
    
    Queue queue = QueueFactory.getQueue("background-processing");
    queue.add(TaskOptions.Builder.url("/tqHandler").param("key", "あいうえお"));
    
    assertEquals(1, delegate.tasks.size());
    TaskQueueAddRequest task = delegate.tasks.get(0);
    assertEquals("/tqHandler", task.getUrl());
    assertEquals("key="+URLEncoder.encode("あいうえお", "utf-8"), task.getBody());
  }

  @Before public void setUp() throws IOException {
    File testFolder = new File("bin/tqtest");
    File queueXml = new File("war/WEB-INF/queue.xml");
    if (queueXml.exists()) {
      FileUtils.copyFile(queueXml, new File("bin/tqtest/WEB-INF/queue.xml"));
    }
    setUpAppEngine(testFolder);
  }

  @After public void tearDown() {
    @SuppressWarnings("unchecked")
    Delegate<Environment> delegate = ApiProxy.getDelegate();
    if (delegate instanceof TaskQueueDelegate) {
      ApiProxy.setDelegate(((TaskQueueDelegate) delegate).apiProxyLocal);
    }
    tearDownAppEngine();
  }

  static void setUpAppEngine(File testFolder) {
    ApiProxyLocal apiProxyLocal = new ApiProxyLocalImpl(testFolder) {};
    Environment environment = new UnitTestEnvironment();
    ApiProxy.setEnvironmentForCurrentThread(environment);
    ApiProxy.setDelegate(apiProxyLocal);
  }

  static void tearDownAppEngine() {
    ApiProxyLocal apiProxyLocal = (ApiProxyLocal) ApiProxy.getDelegate();
    if (apiProxyLocal != null) {
      apiProxyLocal.stop();
    }
    ApiProxy.setDelegate(null);
    ApiProxy.setEnvironmentForCurrentThread(null);
  }
}

ここでは説明をわかりやすくするためにテストメソッド内でTask投入処理を行っていますが、 実際はテスト対象であるサービス層の機能が、内部的にTaskの投入やデータストアサービスを行う事が多いんじゃないかと思います。

コメントを投稿