Test delle unità locali per Java 8

I test di 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. Invece di scrivere test una volta terminato lo sviluppo dell'applicazione, valuta la possibilità di scrivere i test mentre via. Questo ti aiuta a progettare unità di codice di piccole dimensioni, riutilizzabili e gestibili. Inoltre, ti consente di testare il codice in modo accurato e rapido.

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

Uno stub di servizio è un metodo che simula il comportamento del servizio. Per Ad esempio, lo stub del servizio datastore mostrato La scrittura di test Datastore e Memcache ti consente di testare il codice del datastore senza effettuare alcuna richiesta al datastore reale. Qualsiasi entità archiviata durante un test di unità del datastore viene memorizzata in memoria, non nel datastore, ed eliminata al termine dell'esecuzione del test. Puoi eseguire piccoli test rapidi senza alcuna dipendenza dal datastore stesso.

Questo documento fornisce alcune informazioni sulla configurazione di un framework di test, descrive poi come scrivere i test delle unità rispetto a diversi App Engine locali i servizi di machine learning.

Configurazione di un framework di test

Anche se le utilità di test dell'SDK non sono legate a framework specifici, questa guida utilizza JUnit per gli esempi, in modo da qualcosa di concreto e completo su cui lavorare. Prima di iniziare a scrivere i test, dovrai aggiungere il JAR JUnit 4 appropriato al classpath di test. Dopodiché puoi scrivere un prompt molto semplice Test JUnit.


import static org.junit.Assert.assertEquals;

import org.junit.Test;

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

Se utilizzi Eclipse, seleziona il file sorgente del test da eseguire. Seleziona il Menu Esegui > Esegui come > Test JUnit. I risultati del test vengono visualizzati nella finestra della console.

Introduzione alle utilità di test di Java 8

MyFirstTest mostra la configurazione di test più semplice possibile e, per i test che non hanno dipendenze dalle API App Engine o dalle implementazioni dei servizi locali, potresti non aver bisogno di altro. Tuttavia, se i test o il codice in test hanno queste dipendenze, aggiungi i seguenti file JAR al percorso di classe 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 JAR rendono le API di runtime e le implementazioni locali di queste API disponibili per i tuoi test.

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

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

Dai un'occhiata alla documentazione Javadoc del pacchetto com.google.appengine.tools.development.testing. La più importante di questo pacchetto è LocalServiceTestHelper, che gestisce tutta la configurazione necessaria dell'ambiente e ti offre una di configurazione per tutti i servizi locali a cui vuoi accedere i tuoi test.

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

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

Scrittura di test di Datastore e memcache

L'esempio seguente 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 imposta ed elimina le parti l'ambiente di esecuzione comune a tutti i servizi locali e LocalDatastoreServiceTestConfig configura ed elimina le parti del specifici del servizio datastore locale. Se leggi il documento javadoc scoprirai che questo comporta la configurazione del servizio datastore locale per mantenere tutti i dati in memoria (anziché lo svuotamento sul disco a intervalli regolari) e l'eliminazione di tutti i dati in memoria alla fine di ogni test. Questa è solo l'impostazione predefinita il comportamento di un test del datastore e, se questo comportamento non è quello desiderato, modificarlo.

Modifica dell'esempio per accedere a memcache anziché al datastore

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

Anziché importare classi relative al datastore, importa quelle relative a memcache. Devi ancora importare LocalServiceTestHelper.

Cambia il nome della classe che stai creando e cambia 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 effettivamente il test in modo che sia pertinente per 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 il servizio LocalServiceTestConfig specifico (in questo caso LocalMemcacheServiceTestConfig) di gestire l'ambiente di esecuzione.

Scrittura di test di Cloud Datastore

Se la tua app utilizza Cloud Datastore, potresti voler scrivere che verificano il comportamento della tua applicazione a fronte di eventuali coerenza. LocalDatastoreServiceTestConfig offre opzioni che semplificano questa procedura:


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

Impostando la percentuale di job non applicati su 100, diamo al datastore locale l'istruzione di operare con la massima consistenza eventuale. La coerenza finale massima indica che le scritture verranno confermate, ma non verranno mai applicate, pertanto le query globali (non antecedenti) non riusciranno mai a rilevare le modifiche. Questo non è ovviamente 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 il datastore locale in modo che si comporti sempre in questo modo.

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 funzioni correttamente a dispetto della coerenza finale, ma ricorda che l'Alta definizione locale Il modello di coerenza della lettura della replica è un'approssimazione del modello Modello a coerenza di lettura della replica, 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 rende sempre visibili i risultati della scrittura non applicata alle query globali successive. In fase di produzione non è così.

Scrivere test delle code di attività

I test che utilizzano la coda di attività locale sono un po' più complessi perché, a differenza di datastore e memcache, l'API coda di attività non espone un'utilità per esaminare lo stato del servizio. Dobbiamo accedere alla coda delle attività locale stessa 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 di fornire un handle al locale l'istanza del servizio, quindi analizziamo il servizio locale stesso per assicurarci l'attività è stata pianificata come previsto. Tutte le implementazioni di LocalServiceTestConfig espongono un metodo simile. Potresti non averne sempre bisogno, ma prima o poi sarai felice che sia lì.

Impostazione del file di configurazione queue.xml

Le librerie di test delle coda di attività consentono un numero illimitato di queue.xml configurazioni da specificare per ogni LocalServiceTestHelper tramite il metodo LocalTaskQueueTestConfig.setQueueXmlPath. Al momento, qualsiasi impostazione del limite di frequenza della coda viene ignorata dal server di sviluppo locale. Non è possibile eseguire attività simultanee contemporaneamente in locale.

Ad esempio, un progetto potrebbe dover eseguire test in base al queue.xml file che verrà caricato e utilizzato dall'applicazione App Engine. Supponendo che il file queue.xml si trovi nella posizione standard, il codice di esempio riportato sopra potrebbe essere modificato come segue per concedere all'accesso di test 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 per adattarlo alla struttura del file del progetto.

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

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

Scrivere test delle attività differite

Se il codice dell'applicazione utilizza le attività differite, 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 di attività locale, utilizziamo un LocalTaskqueueTestConfig, ma questa volta lo inizializziamo con alcuni argomenti aggiuntivi che ci offrono un modo semplice per verificare non solo che la tarefa è stata pianificata, ma anche che è stata eseguita: chiamiamo setDisableAutoTaskExecution(false) per indicare alla coda di attività locale di eseguire automaticamente le attività. Chiamiamo setCallbackClass(LocalTaskQueueTestConfig.DeferredTaskCallback.class) per indicare alla coda di attività locale di utilizzare un callback che sappia come eseguire le attività posticipate. Infine chiamiamo setTaskExecutionLatch(latch) per dire all'attività locale Metti in coda per ridurre il fermo dopo ogni esecuzione di attività. Questa configurazione ci consente di scrivere un test in cui inseriamo in coda un'attività differita, aspettiamo che l'attività venga eseguita e poi verifichiamo che si sia comportata come previsto durante l'esecuzione.

Scrivere test delle funzionalità dei servizi locali

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

Il seguente snippet di codice imposta lo stato della funzionalità del servizio Datastore su disattivato e poi esegue un test sul servizio. Puoi sostituire altri servizi per il 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 innanzitutto un oggetto Capability inizializzato nel datastore, quindi 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 poi creato utilizzando l'oggetto LocalCapabilitiesServiceTestConfig. Ora che il test è stato configurato, viene creato il DatastoreService e gli viene inviata una query per determinare se il test genera i risultati previsti, in questo caso una CapabilityDisabledException.

Scrittura di test per altri servizi

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

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 o con il ruolo predefinito Amministratore app App Engine ha 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 dell'ambiente relativi all'autenticazione sul LocalServiceTestHelper.

In questo esempio, stiamo configurando LocalServiceTestHelper con LocalUserServiceTestConfig per poter utilizzare OAuthService.