Teste de unidade local para Java 8

O teste de unidade permitirá verificar a qualidade do código depois que você escrevê-lo, mas também é possível usá-lo para melhorar o processo de desenvolvimento à medida que avança. Em vez de escrever testes depois de terminar o desenvolvimento do aplicativo, considere escrevê-los conforme avança. Isso ajuda a projetar unidades de código pequenas, de fácil manutenção e reutilizáveis. Isso também facilita o teste rápido e completo do código.

Ao fazer o teste de unidade local, você executa testes que permanecem dentro do ambiente de desenvolvimento sem envolver componentes remotos. O App Engine oferece utilitários de teste que usam implementações locais de armazenamento de dados e outros serviços do App Engine. Isso significa ser possível fazer o código usar esses serviços localmente, sem implantá-lo no App Engine, usando-se stubs de serviço.

Stub de serviço é um método que simula o comportamento do serviço. Por exemplo, o stub de serviço do armazenamento de dados mostrado em Como escrever testes de Datastore e Memcache permite testar o código do armazenamento de dados sem fazer solicitações para o armazenamento de dados real. Qualquer entidade armazenada durante um teste de unidade do armazenamento de dados é mantida na memória, e não no armazenamento de dados, e será excluída depois da execução do teste. É possível executar testes pequenos e rápidos sem dependência do próprio armazenamento de dados.

Este documento apresenta informações como configurar uma biblioteca de testes e descreve como escrever testes de unidade em vários serviços do App Engine locais.

Como configurar uma biblioteca de testes

Os utilitários de teste do SDK não estão vinculados a nenhuma biblioteca específica, mas este guia usa JUnit para os exemplos. Dessa maneira, você tem algo concreto e completo para trabalhar. Antes de começar a escrever testes, você precisará adicionar o JAR JUnit 4 apropriado ao classpath de teste. Depois que fizer isso, você poderá escrever um teste JUnit muito simples.


import static org.junit.Assert.assertEquals;

import org.junit.Test;

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

Se você estiver executando o Eclipse, selecione o arquivo de origem do teste a ser executado. Selecione o menu Executar > Executar como > Teste JUnit. Os resultados do teste são exibidos na janela do console.

Como apresentar os utilitários de teste em Java 8

MyFirstTest demonstra a configuração de teste mais simples possível e, em testes que não dependam das APIs do App Engine ou implementações de serviços locais, talvez você não precise de mais nada. Entretanto, caso os testes ou o código em teste tenham essas dependências, adicione os seguintes arquivos JAR ao classpath de teste:

  • ${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

Esses JARs disponibilizam as APIs de tempo de execução e as implementações locais dessas APIs para os testes.

Os serviços do App Engine esperam várias coisas do ambiente de execução, e configurá-las envolve uma quantidade razoável de códigos boilerplate. Em vez de configurá-la, você pode usar os utilitários no pacote com.google.appengine.tools.development.testing. Para usar esse pacote, adicione o seguinte arquivo JAR ao classpath de teste:

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

Procure no javadoc o pacote com.google.appengine.tools.development.testing. A classe mais importante neste pacote é LocalServiceTestHelper, que processa toda a configuração de ambiente necessária e oferece um ponto de configuração de nível superior para todos os serviços locais que você queira acessar nos testes.

Para gravar um teste que acesse um serviço local específico:

  • Crie uma instância de LocalServiceTestHelper com uma implementação de LocalServiceTestConfig para esse serviço local específico.
  • Chame setUp() na instância LocalServiceTestHelper antes e tearDown() após cada teste.

Como gravar testes de Memcache e Datastore

O exemplo a seguir testa o uso do serviço 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();
  }
}

Neste exemplo, LocalServiceTestHelper configura e divide as partes do ambiente de execução comuns a todos os serviços locais e LocalDatastoreServiceTestConfig configura e divide as partes do ambiente de execução específicas para o serviço de armazenamento de dados local. Se ler o javadoc, você vai aprender que isso envolve a configuração do serviço de armazenamento de dados local para manter todos os dados na memória (em vez de enviar para disco em intervalos regulares) e limpar todos os dados na memória ao final de cada teste. Esse é apenas o comportamento padrão para um teste de armazenamento de dados e, se esse comportamento não for o desejado, será possível alterá-lo.

Como alterar o exemplo para acessar memcache, em vez do armazenamento de dados

Para criar um teste que acesse o serviço memcache local, você pode usar o código mostrado acima, com algumas pequenas alterações.

Em vez de importar classes relacionadas ao armazenamento de dados, importe as relacionadas a memcache. Você ainda precisa importar LocalServiceTestHelper.

Altere o nome da classe que você está criando e altere a instância de LocalServiceTestHelper. Dessa maneira, eles são específicos do memcache.

public class LocalMemcacheTest {

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

E, por fim, altere a maneira como você efetivamente executa o teste. Dessa maneira, ele é relevante para memcache.

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

Assim como acontece no exemplo do armazenamento de dados, LocalServiceTestHelper e o LocalServiceTestConfig específico do serviço (neste caso, LocalMemcacheServiceTestConfig) gerenciam o ambiente de execução.

Como escrever testes do Cloud Datastore

Se o app usar o Cloud Datastore, será possível escrever testes que verifiquem o comportamento do app diante de uma consistência eventual. LocalDatastoreServiceTestConfig expõe opções que facilitam isso:


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

Definindo a porcentagem de trabalho não aplicado como 100, estamos instruindo o armazenamento de dados local para funcionar com o valor máximo de consistência eventual. Consistência eventual máxima significa que haverá commit das gravações, mas elas sempre deixarão de ser aplicadas. Dessa maneira, consultas globais (não ancestrais) sempre deixarão de ver alterações. Obviamente, isso não representa o volume de consistência eventual que o aplicativo verá em execução na produção, mas, para fins de teste é muito útil ser capaz de configurar o armazenamento de dados local para se comportar assim sempre.

Se quiser um controle mais refinado sobre quais transações deixarão de ser aplicadas, você poderá registrar o próprio 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)));
  }
}

As APIs de teste são úteis para verificar se o aplicativo se comporta corretamente diante de consistência eventual, mas lembre-se de que o modelo de consistência de leitura de alta replicação local é uma aproximação do modelo de consistência de leitura de alta replicação de produção, e não uma réplica exata. No ambiente local, executar um get() de um Entity pertencente a um grupo de entidades com uma gravação não aplicada sempre deixará os resultados da gravação não aplicada visíveis para as consultas globais subsequentes. Isso não ocorre na produção.

Como escrever testes de fila de tarefas

Os testes que usam a fila de tarefas local são um pouco mais complexos porque, diferentemente do armazenamento de dados e do memcache, a API da fila de tarefas não expõe um recurso para examinar o estado do serviço. Precisamos acessar a fila de tarefas local propriamente dita para verificar se uma tarefa foi programada com os parâmetros esperados. Para isso, precisamos de 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();
  }
}

Observe como solicitamos a LocalTaskqueueTestConfig um manipulador para a instância de serviço local e, em seguida, investigamos o próprio serviço local para garantir que a tarefa tenha sido programada conforme o esperado. Todas as implementações de LocalServiceTestConfig expõem um método semelhante. Talvez nem sempre seja necessário, mas você agradecerá por ele estar disponível cedo ou tarde.

Como definir o arquivo de configuração queue.xml

As bibliotecas de teste da fila de tarefas permitem que um número qualquer de configurações queue.xml seja especificado por LocalServiceTestHelper por meio do método LocalTaskQueueTestConfig.setQueueXmlPath. Atualmente, as configurações de limite de taxa de qualquer fila são ignoradas pelo servidor de desenvolvimento local. Não é possível executar tarefas simultâneas localmente.

Por exemplo, um projeto pode precisar ser testado em relação ao arquivo queue.xml que será enviado e usado pelo aplicativo App Engine. Pressupondo-se que o arquivo queue.xml esteja no local padrão, o código de amostra acima pode ser modificado da seguinte maneira para conceder ao teste acesso às filas especificadas no arquivo src/main/webapp/WEB-INF/queue.xml:

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

Modifique o caminho para o arquivo queue.xml a fim de ajustar a estrutura de arquivos do projeto.

Use o método QueueFactory.getQueue para acessar filas por nome:

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

Como escrever testes de tarefa adiada

Se o código do aplicativo usar tarefas diferidas, os utilitários de teste Java facilitarão a gravação de um teste de integração que verifique os resultados dessas tarefas.


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

Assim como acontece com o primeiro exemplo de fila de tarefas locais, estamos usando LocalTaskqueueTestConfig, mas desta vez estamos o inicializando com alguns argumentos adicionais que nos oferecem uma maneira fácil de verificar não apenas se a tarefa foi programada, mas também se ela foi executada: chamamos setDisableAutoTaskExecution(false) para solicitar à fila de tarefas locais a execução automática de tarefas. Chamamos setCallbackClass(LocalTaskQueueTestConfig.DeferredTaskCallback.class) para solicitar que a fila de tarefas local use um callback que entenda como executar tarefas adiadas. E, por fim, chamamos setTaskExecutionLatch(latch) para solicitar à fila de tarefas locais diminuir a trava após a execução de cada tarefa. Essa configuração permite escrever um teste no qual enfileiramos uma tarefa adiada, aguardar a tarefa ser executada e verificar se ela se comportou conforme o esperado quando foi executada.

Como escrever testes de recursos do serviço local

O teste de recursos envolve alterar o status de um serviço, como armazenamento de dados, blobstore, memcache etc. e executar o aplicativo em relação a esse serviço para determinar se o aplicativo está respondendo conforme o esperado em condições diferentes. O status do recurso pode ser alterado usando-se a classe LocalCapabilitiesServiceTestConfig.

O snippet de código a seguir altera o status do recurso do serviço de armazenamento de dados para desativado e executa um teste no serviço de armazenamento de dados. É possível substituir outros serviços do armazenamento de dados conforme necessário.


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

O teste de amostra primeiro cria um objeto Capability inicializado no armazenamento de dados e, em seguida, cria um objeto CapabilityStatus definido como DISABLED. O LocalCapabilitiesServiceTestConfig é criado com o recurso e o status definidos usando-se os objetos Capability e CapabilityStatus recém-criados.

Em seguida, LocalServiceHelper é criado usando-se o objeto LocalCapabilitiesServiceTestConfig. Agora que o teste foi configurado, o DatastoreService é criado e uma consulta é enviada para ele a fim de determinar se o teste gera os resultados esperados, neste caso, um CapabilityDisabledException.

Como escrever testes para outros serviços

Há utilitários de teste disponíveis para blobstore e para outros serviços do App Engine. Para uma lista de todos os serviços que tenham implementações locais para teste, consulte a documentação de LocalServiceTestConfig.

Como escrever testes com expectativas de autenticação

Este exemplo mostra como escrever testes que verificam a lógica que usa UserService para determinar se um usuário se conectou ou tem privilégios de administrador. Qualquer usuário com o papel primário de leitor, editor ou proprietário, ou o papel predefinido de administrador de app do App Engine, tem privilégios 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());
  }
}

Neste exemplo, estamos configurando o LocalServiceTestHelper com o LocalUserServiceTestConfig. Dessa maneira, podemos usar o UserService no teste, mas também estamos configurando alguns dados de ambiente relacionados à autenticação no próprio LocalServiceTestHelper.

Neste exemplo, LocalServiceTestHelper está sendo configurado com o LocalUserServiceTestConfig. Dessa maneira, é possível usar o OAuthService.