Test delle unità locali per Java 8

Il test delle unità consente di verificare la qualità del codice dopo averlo scritto, ma puoi anche utilizzare il test delle unità per migliorare il processo di sviluppo man mano che procedi. Invece di scrivere i test dopo aver completato lo sviluppo dell'applicazione, ti consigliamo di scriverli man mano che procedi. In questo modo, puoi progettare unità di codice piccole, gestibili e riutilizzabili. Inoltre, ti consente di testare il tuo codice in modo rapido e completo in modo più semplice.

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

Uno stub di 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 del datastore senza effettuare richieste al datastore reale. Qualsiasi entità archiviata durante un test delle unità del datastore viene conservata in memoria, non nel datastore, e viene eliminata dopo l'esecuzione del test. Puoi eseguire piccoli test veloci senza alcuna dipendenza dal datastore stesso.

Questo documento fornisce alcune informazioni sulla configurazione di un framework di test e descrive come scrivere test delle unità su diversi servizi App Engine locali.

Configurazione di un framework di test

Anche se le utilità di test dell'SDK non sono legate a un framework specifico, questa guida utilizza JUnit per gli esempi in modo da avere qualcosa di concreto e completo su cui lavorare. Prima di iniziare a scrivere test, devi aggiungere il valore JUnit 4 JAR appropriato al percorso della classe di test. Fatto ciò, puoi scrivere un 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 esegui Eclipse, seleziona il file sorgente del test da eseguire. Seleziona il menu Esegui > Esegui come > JUnit Test. I risultati del test vengono visualizzati nella finestra della console.

Introduzione alle utilità di test Java 8

MyFirstTest dimostra 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 sottoposto a test presentano 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 JAR rendono disponibili per i tuoi test le API di runtime e le implementazioni locali di queste API.

I servizi App Engine si aspettano dall'ambiente di esecuzione varie cose, e la loro configurazione richiede una discreta quantità di codice boilerplate. Anziché eseguire la configurazione autonomamente, puoi utilizzare le utilità incluse 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

Dedica qualche minuto a sfogliare il pacchetto javadoc per trovare 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 offre un punto di configurazione di primo livello per tutti i servizi locali a cui potresti voler accedere nei 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.
  • Richiama setUp() sulla tua istanza LocalServiceTestHelper prima di ogni test e tearDown() dopo ogni test.

Scrittura dei test Datastore e memcache

L'esempio seguente verifica 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 ed elimina le parti dell'ambiente di esecuzione comuni a tutti i servizi locali e LocalDatastoreServiceTestConfig configura ed elimina le parti dell'ambiente di esecuzione specifiche per il servizio datastore locale. Se leggi il documento javadoc, scoprirai che questa operazione comporta la configurazione del servizio datastore locale per mantenere tutti i dati in memoria (anziché eseguire lo svuotamento 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 cambiarlo.

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

Per creare un test che acceda al servizio memcache locale, puoi utilizzare il codice mostrato sopra, apportando 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, cambia 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 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, potresti voler scrivere test che verificano il comportamento dell'applicazione nonostante l'eventuale coerenza. LocalDatastoreServiceTestConfig mostra opzioni che semplificano:


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, indichiamo al datastore locale di funzionare con la massima coerenza finale. Per la massima coerenza finale, le scritture verranno commit, ma non verranno sempre applicate, quindi le query globali (non predecessori) non vedranno costantemente le modifiche. Ovviamente questo non è rappresentativo della quantità di coerenza finale che l'applicazione vedrà durante l'esecuzione in produzione, ma ai fini di test è molto utile poter configurare il datastore locale in modo che si comporti ogni volta 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 nonostante la coerenza finale, ma tieni presente che il modello locale di coerenza di lettura ad alta replica è un'approssimazione del modello di produzione a coerenza di lettura ad alta Replica, non di 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 per le successive query globali. In produzione, non è così.

Scrittura dei test delle coda di attività corso...

I test che utilizzano la coda di attività locale sono un po' più coinvolti perché, a differenza di datastore e memcache, l'API Task Queue non espone una struttura per l'esame dello stato del servizio. Dobbiamo accedere alla coda di attività locale per verificare che sia stata pianificata un'attività 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();
  }
}

Osserva come chiediamo a LocalTaskqueueTestConfig un handle per l'istanza del servizio locale, quindi esaminiamo il servizio locale stesso per assicurarci che l'attività sia stata pianificata come previsto. Tutte le implementazioni LocalServiceTestConfig espongono un metodo simile. Non sempre è necessario, ma prima o poi sarai felice che sia disponibile.

Impostazione del file di configurazione queue.xml

Le librerie di test delle coda di attività consentono di specificare un numero illimitato di configurazioni queue.xml in base a LocalServiceTestHelper tramite il metodo LocalTaskQueueTestConfig.setQueueXmlPath. Attualmente, le impostazioni dei limiti di frequenza di qualsiasi coda vengono ignorate dal server di sviluppo locale. Non è possibile eseguire attività simultanee alla volta in locale.

Ad esempio, potrebbe essere necessario eseguire il test di un progetto 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 sopra riportato può essere modificato come segue per concedere l'accesso di prova 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 dei file del progetto.

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

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

Scrittura di test delle attività differite

Se il codice dell'applicazione utilizza Attività differite, Java Testing Utilities semplifica la scrittura di un test di integrazione che verifichi 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 per il nostro primo esempio di coda di attività locale, utilizziamo LocalTaskqueueTestConfig, ma questa volta lo stiamo inizializzando con alcuni argomenti aggiuntivi che ci permettono di verificare facilmente non solo che l'attività sia stata pianificata, ma che sia 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 capisce come eseguire le attività differite. Infine, chiamiamo setTaskExecutionLatch(latch) per indicare alla coda di attività locale di ridurre il blocco dopo l'esecuzione di ogni attività. Questa configurazione consente di scrivere un test in cui accodare un'attività differita, attendere l'esecuzione dell'attività e quindi verificare che l'attività si comporti come previsto al momento dell'esecuzione.

Scrittura di 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, e l'esecuzione dell'applicazione su quel servizio per determinare se l'applicazione risponde come previsto in condizioni diverse. Lo stato della funzionalità può essere modificato utilizzando la classe LocalCapabilitiesServiceTestConfig.

Lo snippet di codice riportato di seguito modifica lo stato delle funzionalità del servizio datastore impostandolo su Disattivato e poi esegue un test sul servizio. Puoi sostituire altri servizi per il datastore in base alle tue 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 nel 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 l'oggetto DatastoreService e viene inviata una query per determinare se il test genera i risultati attesi, in questo caso un CapabilityDisabledException.

Scrittura di test per altri servizi

Le utilità di test sono disponibili per l'archivio BLOB e altri servizi App Engine. Per un elenco di tutti i servizi con implementazioni locali per i test, consulta la documentazione di LocalServiceTestConfig.

Scrittura di 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 amministrativi. Tieni presente che qualsiasi utente con il ruolo di base Visualizzatore, Editor o Proprietario o con il ruolo predefinito Amministratore app di 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 configurando anche alcuni dati dell'ambiente di autenticazione nel LocalServiceTestHelper stesso.

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