ローカルでの Java 8 の単体テスト

単体テストを行うと、作成したコードの品質を検証するだけでなく、コードの作成を進めながら開発プロセスの改善を行えます。アプリケーションの開発が終わった後にテストを作成するのではなく、開発プロセスと並行して単体テストを作成することを検討してください。これにより、保守が簡単で再利用できる単体コードを作成できます。また、コードのテストをすばやく、徹底的に行うことができます。

ローカルで単体テストを行う場合、ユーザーの開発環境内ですべてのテストを行います。リモートのコンポーネントは使用しません。App Engine のテスト ユーティリティを使用すると、データストアや他の App Engine サービスをローカルに実装できます。コードを App Engine にデプロイする必要はありません。サービススタブにより、これらのサービスをローカルで利用できるようにします。

サービススタブはサービスの動作をシミュレートします。たとえば、データストアと Memcache のテストを作成するで説明するデータストアのサービススタブでは、実際のデータストアにリクエストを送信することなく、データストアのコードをテストできます。データストアの単体テストで生成されるエンティティは、データストアではなくメモリ上に格納され、テストの実行後に削除されます。実際のデータストアに依存することはないため、簡単なテストをすばやく行うことができます。

このドキュメントでは、テスト用のフレームワークについて簡単に説明し、ローカルの App Engine サービスで行う単体テストの作成方法を説明します。

テスト用フレームワークを設定する

SDK のテスト ユーティリティは特定のフレームワークに関連付けられていませんが、この例では JUnit を使用して説明を進めていきます。テストを作成する前に、適切な JUnit 4 JAR をテストのクラスパスに追加してください。追加したら、簡単な JUnit テストを作成してみましょう。


import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class MyFirstTest {
  @Test
  public void testAddition() {
    assertEquals(4, 2 + 2);
  }
}

Eclipse を実行している場合には、テストを行うソースファイルを選択します。[Run] メニュー > [Run As] > [JUnit Test] の順に選択します。テストの結果はコンソール ウィンドウに表示されます。

Java 8 テスト ユーティリティについて

MyFirstTest では、非常に簡単なテストを行います。このテストは、App Engine API やローカルのサービス実装に依存しないため、特に操作を行う必要はありません。ただし、テストやテストで使用するコードにこのような依存関係がある場合には、次の JAR ファイルをテストのクラスパスに追加してください。

  • ${SDK_ROOT}/lib/impl/appengine-api.jar
  • ${SDK_ROOT}/lib/impl/appengine-api-stubs.jar
  • ${SDK_ROOT}/lib/appengine-tools-api.jar

これらの JAR により、ランタイム API やこれらの API のローカル実装をテストで使用できるようになります。

App Engine サービスは、さまざまな実行環境を想定しています。これらの設定を行うには、相当な量のボイラープレート コードが必要になります。自身で設定するのではなく、com.google.appengine.tools.development.testing パッケージ内のユーティリティを使用できます。このパッケージを利用するには、次の JAR ファイルをテストのクラスパスに追加します。

  • ${SDK_ROOT}/lib/testing/appengine-testing.jar

com.google.appengine.tools.development.testing パッケージの javadoc を見てみましょう。このパッケージで最も重要なクラスは LocalServiceTestHelper です。このクラスは、必要な環境の準備をすべて行い、テストで必要になるローカル サービスの構成を行います。

特定のローカル サービスを利用するテストを作成するには:

  • LocalServiceTestHelper のインスタンスを、その特定のローカル サービスの LocalServiceTestConfig 実装で作成します。
  • LocalServiceTestHelper インスタンスで、テストの実行前に setUp() を呼び出し、テストの実行後に tearDown() を呼び出します。

データストアと Memcache のテストを作成する

次の例では、データストア サービスを使用しているかどうかテストします。


import static com.google.appengine.api.datastore.FetchOptions.Builder.withLimit;
import static org.junit.Assert.assertEquals;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class LocalDatastoreTest {

  private final LocalServiceTestHelper helper =
      new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());

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

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

  // Run this test twice to prove we're not leaking any state across tests.
  private void doTest() {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    assertEquals(0, ds.prepare(new Query("yam")).countEntities(withLimit(10)));
    ds.put(new Entity("yam"));
    ds.put(new Entity("yam"));
    assertEquals(2, ds.prepare(new Query("yam")).countEntities(withLimit(10)));
  }

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

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

この例では、実行環境ですべてのローカル サービスに共通する部分の設定と解放を LocalServiceTestHelper が行い、ローカルのデータストア サービスに固有の部分の設定と解放を LocalDatastoreServiceTestConfig が行っています。javadoc の説明のとおり、この処理では、(一定の間隔でディスクにフラッシュするのではなく)すべてのデータをメモリ上に格納し、テストが終了するたびにメモリから消去するようにローカルのデータストア サービスを構成しています。これは、データストア テストのデフォルトの動作ですが、必要に応じて変更できます。

データストアではなく memcache にアクセスするように変更する

テストでローカルの memcache サービスにアクセスするように設定するには、上記のコードを少し変更します。

データストア関連のクラスではなく、memcache 関連のクラスをインポートします。その場合でも、LocalServiceTestHelper をインポートする必要があります。

作成するクラスの名前を変更し、LocalServiceTestHelper のインスタンスを変更して、Memcache に固有になるようにします。

public class LocalMemcacheTest {

  private final LocalServiceTestHelper helper =
      new LocalServiceTestHelper(new LocalMemcacheServiceTestConfig());

最後に、テストの実行方法を変更して、Memcache に関連するようにします。

private void doTest() {
  MemcacheService ms = MemcacheServiceFactory.getMemcacheService();
  assertFalse(ms.contains("yar"));
  ms.put("yar", "foo");
  assertTrue(ms.contains("yar"));
}

データストアの例の場合と同様に、LocalServiceTestHelper とサービス固有の LocalServiceTestConfig(この場合は LocalMemcacheServiceTestConfig)によって実行環境が管理されます。

Cloud Datastore のテストを作成する

アプリで Cloud Datastore を使用する場合は、結果整合性に即してアプリケーションの動作を検証するテストを作成することをおすすめします。これは LocalDatastoreServiceTestConfig のオプションにより簡単になります。


import static com.google.appengine.api.datastore.FetchOptions.Builder.withLimit;
import static org.junit.Assert.assertEquals;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class LocalHighRepDatastoreTest {

  // Maximum eventual consistency.
  private final LocalServiceTestHelper helper =
      new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig()
          .setDefaultHighRepJobPolicyUnappliedJobPercentage(100));

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

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

  @Test
  public void testEventuallyConsistentGlobalQueryResult() {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    Key ancestor = KeyFactory.createKey("foo", 3);
    ds.put(new Entity("yam", ancestor));
    ds.put(new Entity("yam", ancestor));
    // Global query doesn't see the data.
    assertEquals(0, ds.prepare(new Query("yam")).countEntities(withLimit(10)));
    // Ancestor query does see the data.
    assertEquals(2, ds.prepare(new Query("yam", ancestor)).countEntities(withLimit(10)));
  }
}

未適用のジョブの割合を 100 に設定すると、ローカルのデータストアに対し、最大限の結果整合性で動作するよう指示することになります。最大限の結果整合性とは、書き込みが commit されても常に適用に失敗することを意味します。そのため、グローバル(祖先でない)クエリには一貫して、変更が反映されません。これはもちろん、本番環境での実行時にアプリケーションに反映される結果整合性の程度を表すものではありませんが、テストの目的では、ローカル データストアを毎回このように動作するよう構成できれば非常に便利です。

適用しないトランザクションについてより詳細に制御する場合、独自の HighRepJobPolicy を登録できます。


import static com.google.appengine.api.datastore.FetchOptions.Builder.withLimit;
import static org.junit.Assert.assertEquals;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.dev.HighRepJobPolicy;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class LocalCustomPolicyHighRepDatastoreTest {
  private static final class CustomHighRepJobPolicy implements HighRepJobPolicy {
    static int newJobCounter = 0;
    static int existingJobCounter = 0;

    @Override
    public boolean shouldApplyNewJob(Key entityGroup) {
      // Every other new job fails to apply.
      return newJobCounter++ % 2 == 0;
    }

    @Override
    public boolean shouldRollForwardExistingJob(Key entityGroup) {
      // Every other existing job fails to apply.
      return existingJobCounter++ % 2 == 0;
    }
  }

  private final LocalServiceTestHelper helper =
      new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig()
          .setAlternateHighRepJobPolicyClass(CustomHighRepJobPolicy.class));

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

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

  @Test
  public void testEventuallyConsistentGlobalQueryResult() {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    ds.put(new Entity("yam")); // applies
    ds.put(new Entity("yam")); // does not apply
    // First global query only sees the first Entity.
    assertEquals(1, ds.prepare(new Query("yam")).countEntities(withLimit(10)));
    // Second global query sees both Entities because we "groom" (attempt to
    // apply unapplied jobs) after every query.
    assertEquals(2, ds.prepare(new Query("yam")).countEntities(withLimit(10)));
  }
}

テスト用 API は、アプリケーションが結果整合性に即して適切に動作することを検証するために有用ですが、ローカルの高レプリケーション読み取り整合性モデルは、本番環境の高レプリケーション読み取り整合性モデルの近似モデルであり、厳密なレプリカではないことに注意してください。ローカル環境では、適用されない書き込みがあるエンティティ グループに属する Entityget() を実行すると常に、その適用されない書き込みの結果が、以降のグローバル クエリで認識されます。本番環境の場合、これは該当しません。

タスクキューのテストを作成する

ローカルのタスクキューを使用するテストは少し複雑になります。データストアや memcache の場合と異なり、タスクキューの API にはサービスの状態を調べる方法がありません。予想したパラメータでタスクのスケジュールが設定されているかどうか確認するには、ローカルのタスクキューにアクセスする必要があります。これを行うためには、com.google.appengine.api.taskqueue.dev.LocalTaskQueue が必要です。


import static org.junit.Assert.assertEquals;

import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.dev.LocalTaskQueue;
import com.google.appengine.api.taskqueue.dev.QueueStateInfo;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class TaskQueueTest {

  private final LocalServiceTestHelper helper =
      new LocalServiceTestHelper(new LocalTaskQueueTestConfig());

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

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

  // Run this test twice to demonstrate we're not leaking state across tests.
  // If we _are_ leaking state across tests we'll get an exception on the
  // second test because there will already be a task with the given name.
  private void doTest() throws InterruptedException {
    QueueFactory.getDefaultQueue().add(TaskOptions.Builder.withTaskName("task29"));
    // Give the task time to execute if tasks are actually enabled (which they
    // aren't, but that's part of the test).
    Thread.sleep(1000);
    LocalTaskQueue ltq = LocalTaskQueueTestConfig.getLocalTaskQueue();
    QueueStateInfo qsi = ltq.getQueueStateInfo().get(QueueFactory.getDefaultQueue().getQueueName());
    assertEquals(1, qsi.getTaskInfo().size());
    assertEquals("task29", qsi.getTaskInfo().get(0).getTaskName());
  }

  @Test
  public void testTaskGetsScheduled1() throws InterruptedException {
    doTest();
  }

  @Test
  public void testTaskGetsScheduled2() throws InterruptedException {
    doTest();
  }
}

ここでは、タスクが予期されたとおりにスケジュールされたかを確認するため、ローカル サービス インスタンスのハンドルを LocalTaskqueueTestConfig で呼び出し、ローカル サービスを直接調べています。すべての LocalServiceTestConfig 実装で類似したメソッドが公開されています。ここでは必ずしも必要ではありませんが、いずれ必要になります。

queue.xml 構成ファイルの設定

タスクキューのテスト ライブラリを使用すると、LocalTaskQueueTestConfig.setQueueXmlPath メソッドで複数の queue.xml 構成を per-LocalServiceTestHelper に設定できます。現在、ローカルの開発用サーバーでは、キューのレート制限が無視されます。ローカルでは複数のタスクを同時に実行することはできません。

たとえば、App Engine アプリケーションがアップロードして使用する queue.xml ファイルのテストをプロジェクトで行う必要があるとします。queue.xml ファイルが標準的な場所にあると想定すると、上記のサンプルコードを次のように変更して、src/main/webapp/WEB-INF/queue.xml ファイルで指定されているキューへのテストアクセス権を付与できます。

private final LocalServiceTestHelper helper =
    new LocalServiceTestHelper(new LocalTaskQueueTestConfig()
        .setQueueXmlPath("src/main/webapp/WEB-INF/queue.xml"));

プロジェクトのファイル構造に適合するように、queue.xml ファイルへのパスを変更します。

QueueFactory.getQueue メソッドを使用して、キュー名でアクセスします。

QueueFactory.getQueue("my-queue-name").add(TaskOptions.Builder.withTaskName("task29"));

遅延タスクのテストを作成する

アプリケーション コードで遅延タスクを使用している場合、Java テスト ユーティリティを使用すると、これらのタスクの結果を検証する統合テストを簡単に作成できます。


import static org.junit.Assert.assertTrue;

import com.google.appengine.api.taskqueue.DeferredTask;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class DeferredTaskTest {

  // Unlike CountDownLatch, TaskCountDownlatch lets us reset.
  private final LocalTaskQueueTestConfig.TaskCountDownLatch latch =
      new LocalTaskQueueTestConfig.TaskCountDownLatch(1);

  private final LocalServiceTestHelper helper =
      new LocalServiceTestHelper(new LocalTaskQueueTestConfig()
          .setDisableAutoTaskExecution(false)
          .setCallbackClass(LocalTaskQueueTestConfig.DeferredTaskCallback.class)
          .setTaskExecutionLatch(latch));

  private static class MyTask implements DeferredTask {
    private static boolean taskRan = false;

    @Override
    public void run() {
      taskRan = true;
    }
  }

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

  @After
  public void tearDown() {
    MyTask.taskRan = false;
    latch.reset();
    helper.tearDown();
  }

  @Test
  public void testTaskGetsRun() throws InterruptedException {
    QueueFactory.getDefaultQueue().add(
        TaskOptions.Builder.withPayload(new MyTask()));
    assertTrue(latch.await(5, TimeUnit.SECONDS));
    assertTrue(MyTask.taskRan);
  }
}

最初のローカル タスクキューの例と同様に LocalTaskqueueTestConfig を使用していますが、今回は別の引数を追加してキューを初期化しています。これにより、タスクのスケジュールだけでなく、実行状態も簡単に検証できます。ここでは、setDisableAutoTaskExecution(false) を呼び出し、ローカル タスクキューにタスクの自動実行を指示しています。setCallbackClass(LocalTaskQueueTestConfig.DeferredTaskCallback.class) を呼び出し、ローカル タスクキューにコールバックの使用を指示しています。これにより、遅延タスクの実行方法を確認できます。最後に、setTaskExecutionLatch(latch) を呼び出し、各タスクの実行後にラッチをデクリメントするようにローカル タスクキューに指示しています。これにより、遅延タスクをキューに入れて待機し、予期したとおり実行されるかどうか検証するテストを作成できます。

ローカル サービスの機能テストを作成する

機能テストでは、データストア、Blobstore、memcache などのサービスのステータスを変更し、そのサービスでアプリケーションを実行して、異なる条件下でアプリケーションが予期したとおり実行されるかどうかを検証します。機能のステータスを変更するには、LocalCapabilitiesServiceTestConfig クラスを使用します。

次のコード スニペットでは、データストア サービスのステータスを無効にして、データストア サービスのテストを行っています(必要に応じて、データストア以外のサービスも利用できます)。


import static org.junit.Assert.assertEquals;

import com.google.appengine.api.capabilities.Capability;
import com.google.appengine.api.capabilities.CapabilityStatus;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.tools.development.testing.LocalCapabilitiesServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.apphosting.api.ApiProxy;
import org.junit.After;
import org.junit.Test;

public class ShortTest {
  private LocalServiceTestHelper helper;

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

  @Test(expected = ApiProxy.CapabilityDisabledException.class)
  public void testDisabledDatastore() {
    Capability testOne = new Capability("datastore_v3");
    CapabilityStatus testStatus = CapabilityStatus.DISABLED;
    // Initialize the test configuration.
    LocalCapabilitiesServiceTestConfig config =
        new LocalCapabilitiesServiceTestConfig().setCapabilityStatus(testOne, testStatus);
    helper = new LocalServiceTestHelper(config);
    helper.setUp();
    FetchOptions fo = FetchOptions.Builder.withLimit(10);
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    assertEquals(0, ds.prepare(new Query("yam")).countEntities(fo));
  }
}

サンプルテストでは、まず、データストアに初期化された Capability オブジェクトを作成し、CapabilityStatus オブジェクトを作成して DISABLED に設定します。次に、作成した Capability オブジェクトと CapabilityStatus オブジェクトを使用して、機能とステータスが設定された LocalCapabilitiesServiceTestConfig を作成します。

さらに、LocalCapabilitiesServiceTestConfig オブジェクトを使用して、LocalServiceHelper を作成します。これでテストの設定が完了です。作成した DatastoreService にクエリを送信し、予期した結果(この場合は CapabilityDisabledException)が生成されるかどうか確認します。

他のサービスのテストを作成する

テスト ユーティリティは、Blobstore や他の App Engine サービスでも使用できます。テストにローカル実装を使用するすべてのサービスのリストについては、LocalServiceTestConfig のドキュメントをご覧ください。

認証テストを作成する

この例では、テストを作成します。UserService を使用するロジックを検証し、ユーザーがログオンしているかどうかとユーザーに管理者権限があるかどうかを確認します。基本ロールが閲覧者、編集者、オーナーのユーザーや App Engine アプリ管理者の事前定義ロールには管理者権限が設定されています。


import static org.junit.Assert.assertTrue;

import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class AuthenticationTest {

  private final LocalServiceTestHelper helper =
      new LocalServiceTestHelper(new LocalUserServiceTestConfig())
          .setEnvIsAdmin(true).setEnvIsLoggedIn(true);

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

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

  @Test
  public void testIsAdmin() {
    UserService userService = UserServiceFactory.getUserService();
    assertTrue(userService.isUserAdmin());
  }
}

この例では、LocalUserServiceTestConfigLocalServiceTestHelper を構成しているため、テストに UserService を使用できますが、認証関連の環境データを LocalServiceTestHelper 自体に構成することも行います。

この例では、LocalUserServiceTestConfigLocalServiceTestHelper を構成しているため、OAuthService を使用できます。