Test locali delle unità per Java 8

I test delle unità ti consentono di verificare la qualità del codice dopo averlo scritto, ma puoi anche utilizzarli per migliorare il processo di sviluppo man mano che procedi. Anziché scrivere i test al termine dello sviluppo dell'applicazione, valuta la possibilità di scriverli man mano che procedi. In questo modo, puoi progettare unità di codice piccole, gestibili e riutilizzabili. Inoltre, ti consente di testare il codice in modo completo e rapido.

Quando esegui test delle unità locali, esegui test che rimangono all'interno del tuo ambiente di sviluppo senza coinvolgere componenti remoti. App Engine fornisce utilità di test che utilizzano implementazioni locali di Datastore e altri servizi App Engine. Ciò significa che puoi esercitare l'utilizzo di questi servizi da parte del tuo codice localmente, senza eseguire il deployment del codice in App Engine, utilizzando gli stub di servizio.

Uno stub del servizio è un metodo che simula il comportamento del servizio. Ad esempio, lo stub del servizio datastore mostrato in Scrittura di test di Datastore e Memcache ti consente di testare il codice datastore senza effettuare richieste al datastore reale. Qualsiasi entità archiviata durante un test unitario del datastore viene conservata in memoria, non nel datastore, e viene eliminata dopo l'esecuzione del test. Puoi eseguire test piccoli e veloci senza alcuna dipendenza dal datastore stesso.

Questo documento fornisce alcune informazioni sulla configurazione di un framework di test, quindi descrive come scrivere unit test per diversi servizi locali di App Engine.

Configurare un framework di test

Anche se le utilità di test dell'SDK non sono associate a nessun framework specifico, questa guida utilizza JUnit per gli esempi, in modo da avere qualcosa di concreto e completo da cui partire. Prima di iniziare a scrivere i test, devi aggiungere il file JAR di JUnit 4 appropriato al classpath di test. Una volta fatto, puoi scrivere un test JUnit molto semplice.


import static org.junit.Assert.assertEquals;

import org.junit.Test;

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

Se esegui Eclipse, seleziona il file di origine del test da eseguire. Seleziona il menu Esegui > Esegui come > Test JUnit. I risultati del test vengono visualizzati nella finestra della console.

Introduzione delle utilità di test Java 8

MyFirstTest mostra la configurazione di test più semplice possibile e, per i test che non hanno alcuna dipendenza dalle API App Engine o dalle implementazioni di servizi locali, potresti non aver bisogno di altro. Tuttavia, se i test o il codice in fase di test hanno queste dipendenze, aggiungi i seguenti file JAR al classpath di test:

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

Questi file JAR rendono disponibili per i test le API di runtime e le implementazioni locali di queste API.

I servizi App Engine si aspettano una serie di cose dal loro ambiente di esecuzione e la configurazione di queste cose comporta una discreta quantità di codice boilerplate. Anziché configurarlo manualmente, puoi utilizzare le utilità nel pacchetto com.google.appengine.tools.development.testing. Per utilizzare questo pacchetto, aggiungi il seguente file JAR al classpath di test:

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

Prenditi un minuto per sfogliare la javadoc per il pacchetto com.google.appengine.tools.development.testing. La classe più importante di questo pacchetto è LocalServiceTestHelper, che gestisce tutta la configurazione dell'ambiente necessaria e fornisce un punto di configurazione di primo livello per tutti i servizi locali a cui potresti voler accedere nei test.

Per scrivere un test che accede a un servizio locale specifico:

  • Crea un'istanza di LocalServiceTestHelper con un'implementazione LocalServiceTestConfig per quel servizio locale specifico.
  • Chiama setUp() nella tua istanza LocalServiceTestHelper prima di ogni test e tearDown() dopo ogni test.

Scrivere test di Datastore e memcache

Il seguente esempio testa l'utilizzo del servizio 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();
  }
}

In questo esempio, LocalServiceTestHelper configura e smonta le parti dell'ambiente di esecuzione comuni a tutti i servizi locali, mentre LocalDatastoreServiceTestConfig configura e smonta le parti dell'ambiente di esecuzione specifiche del servizio datastore locale. Se leggi la javadoc, scoprirai che ciò comporta la configurazione del servizio datastore locale per conservare tutti i dati in memoria (anziché scaricarli su disco a intervalli regolari) e cancellare tutti i dati in memoria alla fine di ogni test. Questo è solo il comportamento predefinito per un test del datastore e, se non è quello che vuoi, puoi modificarlo.

Modifica dell'esempio per accedere a memcache anziché a Datastore

Per creare un test che acceda al servizio memcache locale, puoi utilizzare il codice mostrato sopra, con alcune piccole modifiche.

Anziché importare le classi correlate a Datastore, importa quelle correlate a memcache. Devi ancora importare LocalServiceTestHelper.

Modifica il nome della classe che stai creando e l'istanza di LocalServiceTestHelper in modo che siano specifiche per memcache.

public class LocalMemcacheTest {

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

Infine, modifica il modo in cui esegui il test in modo che sia pertinente a memcache.

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

Come nell'esempio del datastore, LocalServiceTestHelper e LocalServiceTestConfig specifico del servizio (in questo caso LocalMemcacheServiceTestConfig) gestiscono l'ambiente di esecuzione.

Scrittura di test di Cloud Datastore

Se la tua app utilizza Cloud Datastore, ti consigliamo di scrivere test che verifichino il comportamento dell'applicazione in caso di coerenza finale. LocalDatastoreServiceTestConfig mostra le opzioni che semplificano questa operazione:


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

Se imposti la percentuale di job non applicati su 100, indichi al datastore locale di operare con la massima coerenza finale. La coerenza finale massima indica che le scritture verranno eseguite, ma non verranno mai applicate, pertanto le query globali (non discendenti) non riusciranno a visualizzare le modifiche. Naturalmente, questo non è rappresentativo della quantità di coerenza finale che la tua applicazione vedrà durante l'esecuzione in produzione, ma a scopo di test è molto utile poter configurare l'archivio dati locale in modo che si comporti in questo modo ogni volta.

Se vuoi un controllo più granulare sulle transazioni che non vengono applicate, puoi registrare il tuo 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)));
  }
}

Le API di test sono utili per verificare che l'applicazione si comporti correttamente in caso di coerenza finale, ma tieni presente che il modello di coerenza di lettura High Replication locale è un'approssimazione del modello di coerenza di lettura High Replication di produzione, non una replica esatta. Nell'ambiente locale, l'esecuzione di un get() di un Entity che appartiene a un gruppo di entità con una scrittura non applicata renderà sempre visibili i risultati della scrittura non applicata alle query globali successive. In produzione non è così.

Scrittura di test per le coda di attività

I test che utilizzano la coda di attività locale sono un po' più complessi perché, a differenza di datastore e memcache, l'APIcoda di attivitàe non espone una funzionalità per esaminare lo stato del servizio. Dobbiamo accedere alla coda di attività locale per verificare che un'attività sia stata pianificata con i parametri previsti. Per farlo, abbiamo bisogno di 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();
  }
}

Nota come chiediamo a LocalTaskqueueTestConfig un handle per l'istanza del servizio locale e poi esaminiamo il servizio locale stesso per assicurarci che l'attività sia stata pianificata come previsto. Tutte le implementazioni di LocalServiceTestConfig espongono un metodo simile. Potresti non averne sempre bisogno, ma prima o poi sarai felice di averlo.

Impostazione del file di configurazione queue.xml

Le librerie di test della coda di attività consentono di specificare un numero qualsiasi di configurazioni queue.xml in base a LocalServiceTestHelper tramite il metodo LocalTaskQueueTestConfig.setQueueXmlPath. Al momento, le impostazioni del limite di frequenza di qualsiasi coda vengono ignorate dal server di sviluppo locale. Non è possibile eseguire attività simultanee contemporaneamente in locale.

Ad esempio, un progetto potrebbe dover eseguire test sul file queue.xml che verrà caricato e utilizzato dall'applicazione App Engine. Supponendo che il file queue.xml si trovi nella posizione standard, il codice campione riportato sopra potrebbe essere modificato nel seguente modo per concedere all'utente di test l'accesso alle code specificate nel file src/main/webapp/WEB-INF/queue.xml:

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

Modifica il percorso del file queue.xml in modo che corrisponda alla struttura dei file del progetto.

Utilizza il metodo QueueFactory.getQueue per accedere alle code per nome:

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

Scrittura di test per attività differite

Se il codice dell'applicazione utilizza Deferred Tasks, le utilità di test Java semplificano la scrittura di un test di integrazione che verifica i risultati di queste attività.


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

Come nel primo esempio di coda delle attività locali, utilizziamo un LocalTaskqueueTestConfig, ma questa volta lo inizializziamo con alcuni argomenti aggiuntivi che ci consentono di verificare facilmente non solo che l'attività sia stata pianificata, ma anche che sia stata eseguita: chiamiamo setDisableAutoTaskExecution(false) per indicare alla coda delle attività locali di eseguire automaticamente le attività. Chiamiamo setCallbackClass(LocalTaskQueueTestConfig.DeferredTaskCallback.class) per dire alla coda di attività locale di utilizzare un callback che sappia come eseguire le attività differite. Infine, chiamiamo setTaskExecutionLatch(latch) per indicare alla coda di attività locale di decrementare il latch dopo l'esecuzione di ogni attività. Questa configurazione ci consente di scrivere un test in cui mettiamo in coda un'attività differita, aspettiamo che venga eseguita e poi verifichiamo che si sia comportata come previsto.

Scrivere test delle funzionalità dei servizi locali

Il test delle funzionalità prevede la modifica dello stato di alcuni servizi, ad esempio datastore, blobstore, memcache e così via, e l'esecuzione dell'applicazione rispetto a quel servizio per determinare se l'applicazione risponde come previsto in diverse condizioni. Lo stato della funzionalità può essere modificato utilizzando la classe LocalCapabilitiesServiceTestConfig.

Il seguente snippet di codice modifica lo stato della funzionalità del servizio datastore in disabilitato, quindi esegue un test sul servizio datastore. Puoi sostituire altri servizi a datastore in base alle esigenze.


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

Il test di esempio crea prima un oggetto Capability inizializzato in Datastore, poi crea un oggetto CapabilityStatus impostato su DISABLED. LocalCapabilitiesServiceTestConfig viene creato con la funzionalità e lo stato impostati utilizzando gli oggetti Capability e CapabilityStatus appena creati.

LocalServiceHelper viene quindi creato utilizzando l'oggetto LocalCapabilitiesServiceTestConfig. Ora che il test è stato configurato, viene creato DatastoreService e gli viene inviata una query per determinare se il test genera i risultati previsti, in questo caso un CapabilityDisabledException.

Scrivere test per altri servizi

Sono disponibili utilità di test per Blobstore e altri servizi App Engine. Per un elenco di tutti i servizi che hanno implementazioni locali per i test, consulta la documentazione di LocalServiceTestConfig.

Scrivere test con aspettative di autenticazione

Questo esempio mostra come scrivere test che verificano la logica che utilizza UserService per determinare se un utente ha eseguito l'accesso o dispone dei privilegi di amministratore. Tieni presente che qualsiasi utente con il ruolo di base Visualizzatore, Editor o Proprietario oppure con il ruolo predefinito Amministratore app App Engine dispone di privilegi amministrativi.


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

In questo esempio, stiamo configurando LocalServiceTestHelper con LocalUserServiceTestConfig in modo da poter utilizzare UserService nel nostro test, ma stiamo anche configurando alcuni dati di ambiente correlati all'autenticazione su LocalServiceTestHelper stesso.

In questo esempio, stiamo configurando LocalServiceTestHelper con LocalUserServiceTestConfig in modo da poter utilizzare OAuthService.