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

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

ローカルで単体テストを行う場合、ユーザーの開発環境内ですべてのテストを行います。リモートのコンポーネントは使用しません。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] の順に選択します。テスト結果が [Console] ウィンドウに表示されます。

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

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

  • ${SDK_ROOT}/lib/impl/appengine-api.jar
  • ${SDK_ROOT}/lib/impl/appengine-api-labs.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 です。このクラスは、必要な環境の準備をすべて行い、テストで必要になるローカル サービスの設定を行います。

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

  • 特定のローカル サービス用の LocalServiceTestConfig 実装で LocalServiceTestHelper のインスタンスを作成します。
  • 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 もインポートします。


memcache 固有にするため、作成するクラスの名前と LocalServiceTestHelper のインスタンスを変更します。

public class LocalMemcacheTest {

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

最後に、テストの実行方法を変更します。

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 に設定します。次に、CapabilityCapabilityStatus オブジェクトを使用して、機能とステータスが設定された 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 を使用しています。

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

Java の App Engine スタンダード環境