Prueba de unidades locales para Java 8

La prueba de unidades te permite verificar la calidad del código después de escribirlo. También puedes usarla para mejorar el proceso de desarrollo a medida que avanzas. En lugar de escribir pruebas luego de desarrollar tu aplicación, considera escribirlas conforme avanzas. Esto te ayuda a diseñar unidades de código pequeñas que se pueden mantener y volver a usar. También te facilita probar el código con rapidez y profundidad.

Cuando realices la prueba de unidades local, ejecuta las pruebas que están dentro de tu propio entorno de desarrollo sin involucrar componentes remotos. App Engine proporciona utilidades de prueba que usan implementaciones locales de Datastore y otros servicios de App Engine. Esto significa que puedes usar el código de estos servicios de manera local, sin implementarlo en App Engine, mediante el uso de stubs de servicio.

Un stub de servicio es un método que simula el comportamiento de un servicio. Por ejemplo, el stub de servicio de Datastore que se muestra en Escribe pruebas de Datastore y Memcache te permite probar el código de Datastore sin enviar solicitudes al real. Cualquier entidad almacenada durante una prueba de unidades de Datastore se aloja en la memoria, no en Datastore, y se borra después de ejecutar la prueba. Puedes ejecutar pruebas pequeñas y rápidas sin depender de Datastore.

En este documento se detalla cómo configurar un marco de trabajo de pruebas y cómo escribir pruebas de unidades en los servicios locales de App Engine.

Configura un marco de trabajo de pruebas

Aunque las utilidades de prueba del SDK no están vinculadas a un marco de trabajo específico, esta guía usa JUnit en los ejemplos para que tengas algo concreto y completo con lo que trabajar. Antes de comenzar a escribir pruebas, necesitas agregar el JUnit 4 JAR apropiado a tu ruta de clase de pruebas. Una vez lo consigas, estarás listo para escribir una prueba simple de JUnit.


import static org.junit.Assert.assertEquals;

import org.junit.Test;

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

Si ejecutas Eclipse, selecciona el archivo fuente de la prueba que deseas ejecutar. Selecciona el menú Ejecutar > Ejecutar como > Prueba de JUnit. Los resultados de la prueba aparecen en la ventana de Console.

Ingresa las utilidades de prueba de Java 8

MyFirstTest muestra la configuración de prueba más simple posible y, además, es posible que no necesites nada más para pruebas que no dependan de las API de App Engine o de implementaciones de servicios locales. Sin embargo, si tus pruebas o código bajo prueba tienen estas dependencias, agrega estos archivos JAR a tu ruta de clase de pruebas:

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

Estos JAR hacen que las API de entorno de ejecución y las implementaciones locales de esas API estén disponibles para las pruebas.

Los servicios de App Engine tienen ciertas expectativas de su entorno de ejecución y la configuración de estos implica una buena cantidad de código estándar. En lugar de configurarlo, puedes usar las utilidades del paquete com.google.appengine.tools.development.testing. Para usar este paquete, agrega este archivo JAR a tu ruta de clase de pruebas:

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

Dedica un minuto a explorar el javadoc para buscar el paquete com.google.appengine.tools.development.testing. La clase más importante en este paquete es LocalServiceTestHelper, la cual maneja toda la configuración de entornos necesaria y te brinda un punto de configuración de nivel superior para todos los servicios locales a los que necesites acceder en tus pruebas.

Sigue estos pasos para escribir una prueba con acceso a un servicio local específico:

  • Crea una instancia de LocalServiceTestHelper con una implementación de LocalServiceTestConfig para ese servicio local específico.
  • Llama a setUp() en tu instancia LocalServiceTestHelper antes de cada prueba y tearDown() después de cada prueba.

Escribe pruebas de Datastore y Memcache

En el ejemplo siguiente se prueba el uso del servicio de Datastore.


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();
  }
}

En este ejemplo, LocalServiceTestHelper configura y desglosa las partes del entorno de ejecución que son comunes a todos los servicios locales y LocalDatastoreServiceTestConfig configura y desglosa las partes del entorno de ejecución que son específicas del servicio de almacén de datos local. Si lees el javadoc, verás que esto implica configurar el servicio de Datastore local para mantener todos los datos en la memoria (en lugar de limpiarlo al disco en intervalos regulares) y limpiar todos los datos en la memoria al final de cada prueba. Este es el comportamiento predeterminado de una prueba de Datastore y puedes cambiarlo si no es el que deseas.

Cambia el ejemplo para acceder a Memcache en lugar de Datastore

Para crear una prueba con acceso al servicio de Memcache local, debes usar el código que se muestra arriba, pero con unos pequeños cambios.

En lugar de importar clases relacionadas con Datastore, importa las clases con relación a Memcache. Aún debes importar LocalServiceTestHelper.

Cambia el nombre de la clase que crees y cambia la instancia de LocalServiceTestHelper para que sean específicos de Memcache.

public class LocalMemcacheTest {

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

Y por último, cambia el modo de ejecutar la prueba para que sea relevante a Memcache.

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

Al igual que en el ejemplo del almacén de datos, el LocalServiceTestHelper y la LocalServiceTestConfig específica del servicio (en este caso LocalMemcacheServiceTestConfig) administran el entorno de ejecución.

Escribe pruebas de Cloud Datastore

Si tu app usa Cloud Datastore, puede que quieras escribir pruebas que verifiquen el comportamiento de tu aplicación ante una coherencia eventual. LocalDatastoreServiceTestConfig expone las opciones que facilitan este proceso:


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)));
  }
}

Si se configura el trabajo no aplicado al 100%, se le indica a Datastore local que opere con la cantidad máxima de coherencia eventual. La coherencia eventual máxima significa que las operaciones de escritura se confirmarán, pero su aplicación siempre fallará. Por lo tanto, las consultas globales (no la principal) no podrán ver los cambios de manera constante. Por supuesto, esto no representa la cantidad de coherencia eventual que verá tu aplicación cuando se ejecute en la producción, pero configurar el Datastore local con el fin de que siempre se comporte de esta manera es muy útil para realizar pruebas.

Puedes registrar tu propia HighRepJobPolicy si deseas un control más preciso sobre las transacciones que no se aplican:


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)));
  }
}

Las API de pruebas son útiles para verificar que tu aplicación se comporte de manera correcta en el caso de una coherencia eventual, pero ten en cuenta que el modelo de coherencia de lectura de alta replicación local es una aproximación del modelo de coherencia de lectura de alta replicación de producción, no una réplica exacta. En el entorno local, realizar get() de una Entity que pertenece a un grupo de entidades con una operación de escritura sin aplicar siempre hará que se vean los resultados de esta operación para las consultas globales posteriores. En producción, este no el caso.

Escribe pruebas de lista de tareas en cola

Las pruebas que usan la lista de tareas en cola local están un poco más involucradas porque, a diferencia de Datastore y de Memcache, la API de la lista de tareas en cola no expone una instalación para examinar el estado del servicio. Es necesario acceder a la lista de tareas en cola local para verificar que se programó una tarea con los parámetros esperados. Para hacerlo, necesitamos 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();
  }
}

Observa cómo se le solicita a LocalTaskqueueTestConfig un controlador de la instancia del servicio local y luego investígalo para asegurarte de que la tarea se programó como se esperaba. Todas las implementaciones de LocalServiceTestConfig exponen un método similar. Puede que no siempre lo necesites, pero, tarde o temprano, te alegrará tenerlo allí.

Configura el archivo de configuración queue.xml

Las bibliotecas de prueba de la lista de tareas en cola permiten que se especifique cualquier cantidad de opciones de configuración queue.xml por cada LocalServiceTestHelper a través del método LocalTaskQueueTestConfig.setQueueXmlPath. Ahora, el servidor de desarrollo local ignora la configuración de límite de frecuencia de cualquier cola. No es posible ejecutar tareas simultáneas de manera local.

Por ejemplo, puede que en un proyecto se necesite realizar pruebas con el archivo queue.xml que subirá y usará la aplicación de App Engine. Si suponemos que el archivo queue.xml está en la ubicación estándar, el código de muestra anterior podría modificarse de la siguiente forma para otorgar a la prueba acceso a las colas especificadas en el archivo src/main/webapp/WEB-INF/queue.xml:

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

Modifica la ruta al archivo queue.xml para que se ajuste a la estructura de archivo de tu proyecto.

Usa el método QueueFactory.getQueue para acceder a las colas por nombre:

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

Escribe pruebas de tareas diferidas

Si el código de tu aplicación usa tareas diferidas, las utilidades de pruebas de Java facilitan la escritura de una prueba de integración que verifique los resultados de estas tareas.


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);
  }
}

Al igual que con nuestro primer ejemplo de lista de tareas en cola local, se usa una LocalTaskqueueTestConfig, pero esta vez se inicializa con argumentos adicionales que permitan una manera fácil de verificar que no solo se programó la tarea, sino que también se ejecutó. Luego se llama a setDisableAutoTaskExecution(false) para comunicarle a la lista de tareas en cola local que ejecute tareas de forma automática. Llamamos a setCallbackClass(LocalTaskQueueTestConfig.DeferredTaskCallback.class) para que le comunique a la lista de tareas en cola local que use una devolución de llamada que comprenda cómo ejecutar tareas diferidas. Y por último, hay que llamar a setTaskExecutionLatch(latch) para que le comunique a la lista de tareas en cola local que disminuya los bloqueos temporales después de ejecutar cada tarea. Esta configuración permite que se escriba una prueba en la que se pone en cola una tarea diferida y, después de que se ejecute, verifica que esta se haya comportado como se esperaba durante su ejecución.

Escribe pruebas de capacidades de servicio local

Las pruebas de capacidades implican el cambio del estado de un servicio, como Datastore, Blobstore, Memcache, etc., y ejecutar tu aplicación con el servicio para determinar si responde como se espera en distintas condiciones. El estado de capacidad se puede cambiar con la clase LocalCapabilitiesServiceTestConfig.

El siguiente fragmento de código configura el estado de capacidad del servicio de Datastore como inhabilitado y, luego, ejecuta una prueba en él. Se pueden sustituir otros servicios por Datastore según sea necesario.


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));
  }
}

Primero, la prueba de muestra crea un objeto Capability inicializado en Datastore. Luego, crea un objeto CapabilityStatus configurado como INHABILITADO. El LocalCapabilitiesServiceTestConfig se crea con la capacidad y el estado configurados con los objetos Capability y CapabilityStatus que se acaban de crear.

El LocalServiceHelper se crea mediante el objeto LocalCapabilitiesServiceTestConfig. Ahora que se configuró la prueba, se crea el DatastoreService y se le envía una consulta para determinar si la prueba genera los resultados que se esperan, en este caso, un CapabilityDisabledException.

Escribe pruebas para otros servicios

Hay utilidades de pruebas disponibles para Blobstore y otros servicios de App Engine. Para obtener una lista de todos los servicios que tienen implementaciones locales para probar, consulta la documentación LocalServiceTestConfig.

Escribe pruebas con expectativas de autenticación

En este ejemplo, se muestra cómo escribir pruebas que verifiquen lógica que usa UserService para determinar si un usuario inició sesión o si tiene privilegios de administrador. Ten en cuenta que cualquier usuario con la función básica de lector, editor o propietario, o la función predefinida de administrador de aplicaciones de App Engine tiene privilegios de administrador.


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());
  }
}

En este ejemplo, se configura LocalServiceTestHelper con LocalUserServiceTestConfig para que podamos usar UserService en nuestra prueba, pero también se configuran algunos datos de entornos relacionados con la autenticación en el LocalServiceTestHelper mismo.

En este ejemplo, estamos configurando LocalServiceTestHelper con LocalUserServiceTestConfig para que podamos usar OAuthService.