Nozioni di base per i test

Questo codelab fa parte del corso Advanced Android in Kotlin. Otterrai il massimo valore da questo corso se lavori in sequenza nei codelab, ma non è obbligatorio. Tutti i codelab del corso sono elencati nella pagina di destinazione avanzata per i codelab di Android in Kotlin.

Introduzione

Quando hai implementato la prima funzionalità della tua prima app, è probabile che tu abbia eseguito il codice per verificare che abbia funzionato come previsto. Hai eseguito un test, anche se è stato eseguito un test manuale. Mentre continuavi ad aggiungere e aggiornare le funzionalità, probabilmente hai continuato a eseguire il codice e a verificare che funzioni. ma farlo manualmente ogni volta è stancante, soggetto a errori e non scala.

I computer sono perfetti per la scalabilità e l'automazione. Pertanto, gli sviluppatori di società di grandi e piccole dimensioni scrivono test automatici, ovvero test eseguiti da software e che non richiedono l'utilizzo manuale dell'app per verificare il funzionamento del codice.

In questa serie di codelab imparerai a creare una raccolta di test (nota come suite di test) per un'app reale.

Questo primo codelab illustra i concetti di base dei test su Android, scriverai i tuoi primi test e scoprirai come testare LiveData e ViewModel.

Informazioni importanti

Dovresti acquisire familiarità con:

Obiettivi didattici

Scoprirai i seguenti argomenti:

  • Come scrivere ed eseguire test delle unità su Android
  • Come utilizzare lo sviluppo basato su test
  • Come scegliere i test con strumentazione e i test locali

Scoprirai le seguenti librerie e concetti di codice:

In questo lab proverai a:

  • Configurazione, esecuzione e interpretazione dei test locali e degli strumenti in Android.
  • Scrivere test delle unità in Android utilizzando JUnit4 e Hamcrest.
  • Scrivere test LiveData e ViewModel semplici.

In questa serie di codelab, lavorerai con l'app Notes da fare. L'app ti consente di scrivere le attività da completare e di visualizzarle in un elenco. Puoi quindi contrassegnarli come completati o meno, filtrarli o eliminarli.

Questa app è scritta in Kotlin, ha diversi schermi, utilizza componenti Jetpack e segue l'architettura da una guida all'architettura delle app. Imparando a testare questa app, sarai in grado di testare app che utilizzano le stesse librerie e la stessa architettura.

Per iniziare, scarica il codice:

Scarica Zip

In alternativa, puoi clonare il repository GitHub per il codice:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout starter_code

In questa attività eseguirai l'app ed esplorerai il codebase.

Passaggio 1: esegui l'app di esempio

Dopo aver scaricato l'app TO-DO, aprila in Android Studio ed eseguila. Dovrebbe essere compilata. Esplora l'app procedendo nel seguente modo:

  • Crea una nuova attività con il pulsante più azione. Inserisci prima un titolo e altre informazioni sull'attività. Salvala con il FAB di controllo verde.
  • Nell'elenco delle attività, fai clic sul titolo dell'attività che hai appena completato e controlla la schermata dei dettagli per visualizzare il resto della descrizione.
  • Nell'elenco o nella schermata dei dettagli, seleziona la casella di controllo dell'attività per impostarne lo stato su Completata.
  • Torna alla schermata Attività, apri il menu dei filtri e filtra le attività in base allo stato Attivo e Completato.
  • Apri il riquadro di navigazione a scomparsa e fai clic su Statistiche.
  • Quando torni alla schermata Panoramica, seleziona Cancella completate dal menu del riquadro di navigazione per eliminare tutte le attività con stato Completata.

Passaggio 2: esplora il codice dell'app di esempio

L'app TO-DO si basa sul popolare esempio di architettura e test Architecture Blueprints (utilizzando la versione architettura reattiva del campione). L'app segue l'architettura descritta in una guida all'architettura dell'app. Utilizzo di ViewModels con Fragments, un repository e Room. Se conosci uno degli esempi riportati di seguito, questa app ha un'architettura simile:

È più importante che tu comprenda l'architettura generale dell'app piuttosto che comprendere a fondo la logica di un qualsiasi livello.

Ecco un riepilogo dei pacchetti disponibili:

Pacco: com.example.android.architecture.blueprints.todoapp

.addedittask

La schermata Aggiungi o modifica un'attività: codice dell'interfaccia utente per aggiungere o modificare un'attività.

.data

Livello dati: include il livello dati delle attività. Contiene il codice del database, della rete e del repository.

.statistics

Schermata delle statistiche: codice del livello dell'interfaccia utente per la schermata delle statistiche.

.taskdetail

Schermata Dettagli attività: codice del livello UI per una singola attività.

.tasks

Schermata Attività: codice del livello UI per l'elenco di tutte le attività.

.util

Corsi di utilità: corsi condivisi utilizzati in diverse parti dell'app, ad esempio per il layout di aggiornamento dello scorrimento utilizzato su più schermate.

Livello dati (.data)

Questa app include un livello di rete simulato, nel pacchetto remote, e un livello database, nel pacchetto local. Per semplicità, in questo progetto il livello di networking viene simulato con un semplice HashMap, con un ritardo, anziché effettuare richieste di rete reali.

Le coordinate o le mediazioni di DefaultTasksRepository tra il livello di networking e il livello di database sono i dati che restituiscono i dati al livello dell'interfaccia utente.

Livello UI ( .aggiungereittask, .statistic, .taskdetail, .tasks)

Ciascuno dei pacchetti di livelli UI contiene un frammento e un modello di visualizzazione, insieme alle altre classi necessarie per l'interfaccia utente (ad esempio un adattatore per l'elenco delle attività). TaskActivity è l'attività che contiene tutti i frammenti.

Navigazione

La navigazione per l'app è controllata dal componente Navigazione. È definito nel file nav_graph.xml. La navigazione viene attivata nei modelli delle viste utilizzando la classe Event; anche i modelli di vista determinano gli argomenti da trasmettere. I frammenti osservano i Event e si spostano effettivamente tra le schermate.

In questa attività, eseguirai i primi test.

  1. In Android Studio, apri il riquadro Progetto e individua le tre cartelle seguenti:
  • com.example.android.architecture.blueprints.todoapp
  • com.example.android.architecture.blueprints.todoapp (androidTest)
  • com.example.android.architecture.blueprints.todoapp (test)

Queste cartelle sono note come set di origini. I set di origine sono cartelle contenenti codice sorgente della tua app. I set di origini di colore verde (androidTest e test) contengono i tuoi test. Per impostazione predefinita, quando crei un nuovo progetto Android vengono visualizzati i seguenti tre set di origini. Sono:

  • main: contiene il codice della tua app. Questo codice è condiviso tra tutte le diverse versioni dell'app che puoi creare (note come varianti della build)
  • androidTest: contiene test chiamati test strumentali.
  • test: contiene test noti come test locali.

La differenza tra test locali e test con strumenti viene valutata nel modo in cui vengono eseguiti.

Test locali (test set di origini)

Questi test vengono eseguiti localmente sulla JVM della tua macchina di sviluppo e non richiedono un emulatore o un dispositivo fisico. Per questo motivo, corrono velocemente, ma la loro fedeltà è inferiore, il che significa che agiscono meno come nel mondo reale.

In Android Studio, i test locali sono rappresentati da un'icona triangolare verde e rossa.

Test con strumentazione (androidTest set di origini)

Questi test vengono eseguiti su dispositivi Android reali o emulati, quindi riflettono ciò che accadrà nel mondo reale, ma sono anche molto più lenti.

I test con strumenti di Android Studio sono rappresentati da un'icona Android con un triangolo verde e un'icona rossa.

Passaggio 1: esegui un test locale

  1. Apri la cartella test finché non trovi il file ExampleUnitTest.kt.
  2. Fai clic con il tasto destro del mouse e seleziona Esegui ExampleUnitTest.

Dovresti vedere il seguente output nella finestra Run (Esegui) nella parte inferiore dello schermo:

  1. Osserva i segni di spunta verdi ed espandi i risultati del test per confermare che un test chiamato addition_isCorrect sia stato superato. È molto utile sapere che l'aggiunta funziona come previsto.

Passaggio 2: il test non viene superato

Di seguito è riportato il test che hai eseguito.

ExampleUnitTest.kt

// A test class is just a normal class
class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       // Here you are checking that 4 is the same as 2+2
       assertEquals(4, 2 + 2)
   }
}

Nota che i test

  • sono una classe di uno dei set di origini di test.
  • contengono funzioni che iniziano con l'annotazione @Test (ogni funzione è un singolo test).
  • {0}Di solito contengono dichiarazioni asseritive.

Android utilizza la libreria di test JUnit per i test (in questo codelab JUnit4). Sia le asserzioni che l'annotazione @Test provengono da JUnit.

L'elemento principale del test è una valutazione. È un'istruzione di verifica del codice che verifichi il comportamento del codice o dell'app come previsto. In questo caso, l'asserzione è assertEquals(4, 2 + 2) che verifica che 4 sia uguale a 2 + 2.

Per scoprire come funziona un test in errore, aggiungi un'affermazione che puoi facilmente individuare non dovrebbe riuscire. Controllo 3 è uguale a 1+1.

  1. Aggiungi assertEquals(3, 1 + 1) al test addition_isCorrect.

ExampleUnitTest.kt

class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 2)
       assertEquals(3, 1 + 1) // This should fail
   }
}
  1. Esegui il test.
  1. Nei risultati, controlla la presenza di una X accanto al test.

  1. Nota:
  • Una singola asserzione non riuscita non supera l'intero test.
  • Ti viene indicato il valore previsto (3) rispetto al valore calcolato effettivamente (2).
  • Il sistema ti reindirizzerà alla riga dell'asserzione non riuscita (ExampleUnitTest.kt:16).

Passaggio 3: esegui un test con strumentazione

I test con strumentazione fanno parte del set di origini androidTest.

  1. Apri il set di origini androidTest.
  2. Esegui il test ExampleInstrumentedTest.

ExampleInstrumentedTest

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.android.architecture.blueprints.reactive",
            appContext.packageName)
    }
}

A differenza del test locale, questo test viene eseguito su un dispositivo (nell'esempio riportato di seguito, su un telefono Pixel 2 emulato):

Se hai un dispositivo collegato o un emulatore in esecuzione, dovresti vedere il test in esecuzione sull'emulatore.

In questa attività scriverai test per getActiveAndCompleteStats, che calcolano la percentuale di statistiche sulle attività attive e complete relative alla tua app. Puoi visualizzare questi numeri nella schermata delle statistiche dell'app.

Passaggio 1: crea un corso di prova

  1. Nel set di origine di main, in todoapp.statistics, apri StatisticsUtils.kt.
  2. Trova la funzione getActiveAndCompletedStats.

Statistiche Util.kt

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)

La funzione getActiveAndCompletedStats accetta un elenco di attività e restituisce un StatsResult. StatsResult è una classe di dati contenente due numeri, la percentuale di attività completate e la percentuale attiva.

Android Studio ti fornisce gli strumenti per generare stub di test che ti aiutano a implementare i test per questa funzione.

  1. Fai clic con il pulsante destro del mouse su getActiveAndCompletedStats e seleziona Genera > Test.

Viene visualizzata la finestra di dialogo Crea test:

  1. Cambia il Nome del corso: in StatisticsUtilsTest (anziché StatisticsUtilsKtTest; è preferibile non utilizzare KT nel nome del corso).
  2. Mantieni le altre impostazioni predefinite. JUnit 4 è la libreria di test appropriata. Il pacchetto di destinazione è corretto (in base alla posizione della classe StatisticsUtils) e non è necessario selezionare alcuna casella di controllo (questa opzione ti permette di generare un codice aggiuntivo, ma il test verrà scritto da zero).
  3. Premi OK

Si apre la finestra di dialogo Choose Destination Directory (Scegli directory di destinazione):

Verificherai un test locale perché la tua funzione esegue calcoli matematici e non includerà alcun codice specifico per Android. Pertanto, non è necessario eseguirla su un dispositivo reale o emulato.

  1. Seleziona la directory test (non androidTest) perché scriverai i test locali.
  2. Fai clic su OK.
  3. Osserva la classe StatisticsUtilsTest generata in test/statistics/.

Passaggio 2: scrivi la prima funzione di test

Stai per scrivere un test che verifica:

  • se non ci sono attività completate e un'attività attiva
  • che la percentuale di test attivi sia del 100%,
  • e la percentuale di attività completate è pari allo 0%.
  1. Apri StatisticsUtilsTest.
  2. Crea una funzione denominata getActiveAndCompletedStats_noCompleted_returnsHundredZero.

StatisticheUtils.kt

class StatisticsUtilsTest {

    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
        // Create an active task

        // Call your function

        // Check the result
    }
}
  1. Aggiungi l'annotazione @Test sopra il nome della funzione per indicare che si tratta di un test.
  2. Crea un elenco di attività.
// Create an active task 
val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
  1. Chiama getActiveAndCompletedStats con queste attività.
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. Utilizzando le asserzioni, verifica che result sia quello che ti aspettavi.
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

Ecco il codice completo.

StatisticheUtils.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {

        // Create an active task (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertEquals(result.completedTasksPercent, 0f)
        assertEquals(result.activeTasksPercent, 100f)
    }
}
  1. Esegui il test (fai clic con il pulsante destro del mouse su StatisticsUtilsTest e seleziona Esegui).

L'account dovrebbe superare:

Passaggio 3: aggiungi la dipendenza Hamcrest

Dato che i tuoi test agiscono come documentazione di ciò che fa il tuo codice, è utile quando sono leggibili da una persona. Confronta le due affermazioni seguenti:

assertEquals(result.completedTasksPercent, 0f)

// versus

assertThat(result.completedTasksPercent, `is`(0f))

La seconda affermazione è molto più simile a una frase umana. La struttura utilizza un framework di asserzioni chiamato Hamcrest. Un altro valido strumento per scrivere asserzioni leggibili è la Libreria Truth. Utilizzerai Hamcrest in questo codelab per scrivere affermazioni.

  1. Apri build.grade (Module: app) e aggiungi la seguente dipendenza.

app/build.gradle

dependencies {
    // Other dependencies
    testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}

In genere utilizzi implementation per aggiungere una dipendenza, ma qui utilizzi testImplementation. Quando sei pronto a condividere la tua app con il mondo intero, è preferibile non gonfiare le dimensioni dell'APK con qualsiasi codice o dipendenza di test nella tua app. Puoi stabilire se una libreria deve essere inclusa nel codice principale o di test utilizzando le configurazioni gradle. Le configurazioni più comuni sono:

  • implementation: la dipendenza è disponibile in tutti i set di origini, inclusi quelli di test.
  • testImplementation: la dipendenza è disponibile solo nell'insieme di origini di test.
  • androidTestImplementation: la dipendenza è disponibile solo nel set di origini androidTest.

La configurazione che utilizzi definisce dove può essere utilizzata la dipendenza. Se scrivi:

testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"

Ciò significa che Hamcrest sarà disponibile solo nell'insieme di fonti di test. Garantisce inoltre che Hamcrest non sia incluso nella tua app finale.

Passaggio 4: utilizza Hamcrest per scrivere affermazioni

  1. Aggiorna il test getActiveAndCompletedStats_noCompleted_returnsHundredZero() per utilizzare assertThat di Hamcrest anziché assertEquals.
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))

Tieni presente che puoi utilizzare l'importazione import org.hamcrest.Matchers.`is`, se richiesto.

Il test finale avrà il seguente aspetto.

StatisticheUtils.kt

import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {

        // Create an active tasks (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))

    }
}
  1. Esegui il test aggiornato per verificare che funzioni ancora.

Questo codelab non ti mostrerà tutti i dettagli di Hamcrest, quindi se vuoi saperne di più, guarda il tutorial ufficiale.

Questa è un'attività facoltativa facoltativa.

In questa attività dovrai scrivere altri test utilizzando JUnit e Hamcrest. Scrivi i test utilizzando anche una strategia derivata dalla prassi del programma Test Driven Development (Sviluppo guidato da test). Lo sviluppo basato su test TDD è una scuola di programmazione che afferma che prima di scrivere il codice delle funzionalità devi prima scrivere i test. Dopodiché scrivi il codice della funzionalità con l'obiettivo di superare i test.

Passaggio 1. Scrivere i test

Scrivi i test quando hai un normale elenco di attività:

  1. Se è presente un'attività completata e nessuna attività attiva, la percentuale di activeTasks deve essere pari a 0f e la percentuale di attività completate deve essere 100f.
  2. Se ci sono due attività completate e tre attività attive, la percentuale completata deve essere 40f e la percentuale attiva dovrebbe essere 60f.

Passaggio 2. Scrivere un test per un bug

Il codice per il getActiveAndCompletedStats come scritto presenta un bug. Nota come non gestisce correttamente cosa succede se l'elenco è vuoto o nullo. In entrambi i casi, entrambe le percentuali devono essere pari a zero.

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

Per correggere il codice e scrivere i test, devi utilizzare lo sviluppo basato su test. Sviluppo basato su test segue questi passaggi.

  1. Scrivere il test utilizzando la struttura "Data", "Quando", "Dopo" e "Con un nome conforme alla convenzione".
  2. Verifica che la verifica non vada a buon fine.
  3. Scrivi il codice minimo per superare il test.
  4. Ripeti l'operazione per tutti i test.

Anziché iniziare a correggere il bug, dovrai iniziare a scrivere i test. Successivamente puoi confermare di avere test che ti proteggeranno dalla reintroduzione accidentale di questi bug in futuro.

  1. Se è presente un elenco vuoto (emptyList()), entrambe le percentuali devono essere 0f.
  2. Se si è verificato un errore durante il caricamento delle attività, l'elenco sarà null ed entrambe le percentuali devono essere 0f.
  3. Esegui i test e verifica che non superino:

Passaggio 3. Correggere

Ora che hai le verifiche, correggi il bug.

  1. Correggi il bug in getActiveAndCompletedStats restituendo 0f se tasks è null o vuoto:
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}
  1. Esegui nuovamente i test e verifica che vengano superati tutti.

Se segui TDD e scrivi prima i test, hai verificato di:

  • Le nuove funzionalità sono sempre associate a test, pertanto i test agiscono da documentazione su ciò che fa il tuo codice.
  • I tuoi test verificano la correttezza dei risultati e proteggono da bug che hai già visto.

Soluzione: scrivere più test

Di seguito sono riportati tutti i test e il codice funzione corrispondente.

StatisticheUtils.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
        val tasks = listOf(
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed with an active task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 100 and 0
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
        val tasks = listOf(
            Task("title", "desc", isCompleted = true)
        )
        // When the list of tasks is computed with a completed task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 0 and 100
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(100f))
    }

    @Test
    fun getActiveAndCompletedStats_both_returnsFortySixty() {
        // Given 3 completed tasks and 2 active tasks
        val tasks = listOf(
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = false),
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed
        val result = getActiveAndCompletedStats(tasks)

        // Then the result is 40-60
        assertThat(result.activeTasksPercent, `is`(40f))
        assertThat(result.completedTasksPercent, `is`(60f))
    }

    @Test
    fun getActiveAndCompletedStats_error_returnsZeros() {
        // When there's an error loading stats
        val result = getActiveAndCompletedStats(null)

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_empty_returnsZeros() {
        // When there are no tasks
        val result = getActiveAndCompletedStats(emptyList())

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }
}

Statistiche Util.kt

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}

Ottimo lavoro con le nozioni di base sulla scrittura e sull'esecuzione dei test. Dopodiché imparerai a scrivere test di base ViewModel e LiveData.

Nel resto del codelab, imparerai a scrivere test per due classi Android comuni nella maggior parte delle app: ViewModel e LiveData.

Inizi scrivendo i test per TasksViewModel.


Ci occuperai dei test che hanno tutte la logica nel modello di visualizzazione e non si basano sul codice del repository. Il codice del repository prevede codice asincrono, database e chiamate di rete, che aggiungono complessità al test. Evitarai per il momento di concentrarti sulla scrittura di test per la funzionalità ViewModel che non testerà direttamente nulla nel repository.



Il test che scriverai verificherà che, quando chiami il metodo addNewTask, il Event per l'apertura della nuova finestra dell'attività viene attivato. Ecco il codice dell'app che proverai.

TasksViewModel.kt

fun addNewTask() {
   _newTaskEvent.value = Event(Unit)
}

Passaggio 1. Creare una classe TasksViewModelTest

Seguendo gli stessi passaggi descritti per StatisticsUtilTest, in questo passaggio creerai un file di test per TasksViewModelTest.

  1. Apri il corso che vuoi testare nel pacchetto tasks, TasksViewModel.
  2. Nel codice, fai clic con il pulsante destro del mouse sul nome del corso TasksViewModel -> Generate -> Test.

  1. Nella schermata Create Test (Crea test), fai clic su OK per accettare (non dovrai modificare alcuna impostazione predefinita).
  2. Nella finestra di dialogo Choose Directory Directory (Scegli directory di destinazione), scegli la directory test.

Passaggio 2. Inizia a scrivere il test del modello di visualizzazione

In questo passaggio aggiungi un test del modello di visualizzazione per verificare che, quando chiami il metodo addNewTask, viene attivato l'elemento Event per aprire la nuova finestra di attività.

  1. Crea un nuovo test chiamato addNewTask_setsNewTaskEvent.

TasksViewModelTest.kt

class TasksViewModelTest {

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel


        // When adding a new task


        // Then the new task event is triggered

    }
    
}

Cosa succede con il contesto delle applicazioni?

Quando crei un'istanza di TasksViewModel per testare, il suo costruttore richiede un contesto delle applicazioni. Ma in questo test non stai creando un'applicazione completa con attività, UI e frammenti, quindi come puoi ottenere un contesto per l'applicazione?

TasksViewModelTest.kt

// Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(???)

Le librerie di test AndroidX includono classi e metodi che ti forniscono versioni dei componenti come applicazioni e attività destinate ai test. Quando disponi di un test locale in cui ti servono classi di framework Android simulate(come il contesto di un'applicazione), segui questi passaggi per configurare correttamente AndroidX Test:

  1. Aggiungi le dipendenze di base e delle estensioni AndroidX Test
  2. Aggiungere la dipendenza Robolectric Testing Library
  3. Annota la classe con il runner per AndroidJunit4
  4. Scrivere il codice di test AndroidX

Completa questi passaggi per poi capire come funzionano insieme.

Passaggio 3. Aggiungi le dipendenze del Gradle

  1. Copia queste dipendenze nel file build.gradle del modulo della tua app per aggiungere le dipendenze principali dell'esperimento AndroidX di base, nonché la dipendenza Robolectric test.

app/build.gradle

    // AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"

    testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"

 testImplementation "org.robolectric:robolectric:$robolectricVersion"

Passaggio 4. Aggiunta esecutore test JUnit

  1. Aggiungi @RunWith(AndroidJUnit4::class)sopra la classe del test.

TasksViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

Passaggio 5. Usa AndroidX Test

A questo punto, puoi utilizzare la libreria di test di AndroidX. Viene incluso il metodo ApplicationProvider.getApplicationContext, che riceve un contesto applicazione.

  1. Crea un elemento TasksViewModel con ApplicationProvider.getApplicationContext() dalla libreria di test di AndroidX.

TasksViewModelTest.kt

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. Chiama addNewTask al numero tasksViewModel.

TasksViewModelTest.kt

tasksViewModel.addNewTask()

A questo punto, il test dovrebbe essere simile al seguente codice.

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
  1. Esegui il test per confermare che funzioni.

Concetto: come funziona AndroidX Test?

Che cos'è AndroidX Test?

AndroidX Test è una raccolta di librerie per i test. Include classi e metodi che ti offrono versioni di componenti come applicazioni e attività, pensati per i test. Ad esempio, questo codice è un esempio di una funzione di test di AndroidX per ottenere un contesto di applicazione.

ApplicationProvider.getApplicationContext()

Uno dei vantaggi delle API AndroidX Test è che sono progettati per funzionare sia per i test locali sia per i test strumentati. Questo perché:

  • Puoi eseguire lo stesso test di un test locale o di una strumentazione.
  • Non è necessario imparare diverse API di test per i test locali e con strumento.

Ad esempio, perché hai scritto il codice con le librerie di test di AndroidX, puoi spostare la classe TasksViewModelTest dalla cartella test alla cartella androidTest e i test verranno comunque eseguiti. Il getApplicationContext() funziona in modo leggermente diverso a seconda che sia eseguito come test locale o con strumento:

  • Se è un test con strumentazione, il contesto dell'applicazione viene fornito all'avvio di un emulatore o tramite un dispositivo reale.
  • Se si tratta di un test locale, viene utilizzato un ambiente Android simulato.

Che cos'è Robolectric?

L'ambiente Android simulato utilizzato da AndroidX Test per i test locali è fornito da Robolectric. Robolectric è una libreria che crea un ambiente Android simulato per i test e viene eseguita più velocemente rispetto all'avvio di un emulatore o all'esecuzione su un dispositivo. Senza la dipendenza Robolectric, verrà visualizzato questo errore:

A cosa serve @RunWith(AndroidJUnit4::class)?

Un test runner è un componente di JUnit che esegue i test. Senza un esecutore, i test non verrebbero eseguiti. C'è un runner predefinito fornito da JUnit che ricevi automaticamente. @RunWith sostituisce il runner predefinito.

L'esecutore del test di AndroidJUnit4 consente ad AndroidX Test di eseguire il test in modo diverso, a seconda del fatto che siano testati con la strumentazione o test locali.

Passaggio 6. Correggi gli avvisi Robolectric

Quando esegui il codice, nota che viene utilizzato Robolectric.

Grazie ad AndroidX Test e al runner per AndroidJunit4, è possibile farlo senza che tu debba scrivere direttamente una sola riga di codice Robolectric.

Potresti notare due avvisi.

  • No such manifest file: ./AndroidManifest.xml
  • "WARN: Android SDK 29 requires Java 9..."

Puoi correggere l'avviso No such manifest file: ./AndroidManifest.xml aggiornando il file gradle.

  1. Aggiungi la seguente riga al file Gradle in modo che venga utilizzato il file manifest Android corretto. L'opzione includeAndroidResources ti consente di accedere alle risorse Android nei test delle unità, incluso il file AndroidManifest.

app/build.gradle

    // Always show the result of every unit test when running via command line, even if it passes.
    testOptions.unitTests {
        includeAndroidResources = true

        // ... 
    }

L'avviso "WARN: Android SDK 29 requires Java 9..." è più complicato. L'esecuzione dei test su Android Q richiede Java 9. Anziché provare a configurare Android Studio in modo da utilizzare Java 9, per il codelab mantieni il target e compila l'SDK a 28.

In sintesi:

  • I test del modello di visualizzazione pura possono in genere essere effettuati nel set di sorgenti test perché il loro codice di solito non richiede Android.
  • Puoi utilizzare il test di AndroidXdella libreria per ottenere versioni di test di componenti come Applicazioni e attività.
  • Se devi eseguire codice Android simulato nel set di origini test, puoi aggiungere la dipendenza Robolectric e l'annotazione @RunWith(AndroidJUnit4::class).

Congratulazioni, stai utilizzando la libreria di test di AndroidX e Robolectric per eseguire un test. Il tuo test non è terminato (non hai ancora scritto un'affermazione, ora si tratta solo di // TODO test LiveData). Imparerai a scrivere le dichiarazioni rivendicate con LiveData in seguito.

In questa attività, imparerai come rivendicare correttamente il valore LiveData.

Ecco da dove avevi interrotto addNewTask_setsNewTaskEvent per visualizzare il test del modello.

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
    

Per testare LiveData, ti consigliamo di svolgere due operazioni:

  1. Usa InstantTaskExecutorRule
  2. Assicurati che LiveData osservazione

Passaggio 1. Usa InstantTaskExecutorRule

InstantTaskExecutorRule è una regola JUnit. Quando lo utilizzi con l'annotazione @get:Rule, viene eseguito parte del codice nella classe InstantTaskExecutorRule prima e dopo i test (per vedere il codice esatto, puoi usare la scorciatoia da tastiera Comando+B per visualizzare il file).

Questa regola esegue tutti i job in background relativi ai componenti dell'architettura nello stesso thread in modo che i risultati del test vengano eseguiti in modo sincrono e in un ordine ripetibile. Quando scrivi test che includono test in tempo reale, utilizza questa regola.

  1. Aggiungi la dipendenza Gradle per la Libreria di test dei componenti dell'architettura (che contiene questa regola).

app/build.gradle

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
  1. Apri TasksViewModelTest.kt
  2. Aggiungi InstantTaskExecutorRule all'interno della classe TasksViewModelTest.

TasksViewModelTest.kt

class TasksViewModelTest {
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
    // Other code...
}

Passaggio 2. Aggiungi la classe LiveDataTestUtil.kt

Il passaggio successivo consiste nell'assicurarti di eseguire l'esame del LiveData.

Quando utilizzi LiveData, in genere hai un'attività o un frammento (LifecycleOwner) osserva il LiveData.

viewModel.resultLiveData.observe(fragment, Observer {
    // Observer code here
})

Questa osservazione è importante. Devi avere osservatori attivi su LiveData per

  • attivare qualsiasi evento onChanged.
  • attivare qualsiasi Trasformazione.

Per ottenere il comportamento LiveData previsto per il modello di vista LiveData, devi osservare il LiveData con un LifecycleOwner.

Questo rappresenta un problema: nel test TasksViewModel non hai attività o frammenti per osservare il tuo LiveData. Per aggirare il problema, puoi utilizzare il metodo observeForever, che garantisce che LiveData venga costantemente osservato, senza bisogno di una LifecycleOwner. Quando observeForever, devi ricordarti di rimuovere la persona che la osserva o di rischiare una fuga di dati.

Il codice è simile al seguente. Esaminalo:

@Test
fun addNewTask_setsNewTaskEvent() {

    // Given a fresh ViewModel
    val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())


    // Create observer - no need for it to do anything!
    val observer = Observer<Event<Unit>> {}
    try {

        // Observe the LiveData forever
        tasksViewModel.newTaskEvent.observeForever(observer)

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.value
        assertThat(value?.getContentIfNotHandled(), (not(nullValue())))

    } finally {
        // Whatever happens, don't forget to remove the observer!
        tasksViewModel.newTaskEvent.removeObserver(observer)
    }
}

C'è molto codice boilerplate per osservare un singolo LiveData in un test. Esistono diversi modi per eliminare questa caldaia. Creerai una funzione di estensione chiamata LiveDataTestUtil per semplificare l'aggiunta degli osservatori.

  1. Crea un nuovo file Kotlin denominato LiveDataTestUtil.kt nel tuo set di origini test.


  1. Copia e incolla il codice qui sotto.

LiveDataTestUtil.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException


@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

Si tratta di un metodo piuttosto complicato. Crea una funzione di estensione Kotlin denominata getOrAwaitValue che aggiunge un osservatore, ottiene il valore LiveData e quindi ripulisce l'osservatore, in pratica una breve versione riutilizzabile del codice observeForever mostrato sopra. Per una spiegazione completa di questo corso, consulta questo post del blog.

Passaggio 3. Usare getOrAwaitValue per scrivere l'asserzione

In questo passaggio, devi utilizzare il metodo getOrAwaitValue e scrivere un'istruzione di dichiarazione che verifichi che newTaskEvent è stato attivato.

  1. Ricevi il valore LiveData per newTaskEvent utilizzando getOrAwaitValue.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. Dichiara che il valore non sia null.
assertThat(value.getContentIfNotHandled(), (not(nullValue())))

Il test completo dovrebbe avere il seguente codice.

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()


    @Test
    fun addNewTask_setsNewTaskEvent() {
        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.getOrAwaitValue()

        assertThat(value.getContentIfNotHandled(), not(nullValue()))


    }

}
  1. Esegui il codice e guarda il pass di prova.

Ora che hai esaminato come scrivere un test, creane uno in autonomia. In questo passaggio, utilizzando le competenze apprese, fai pratica con la scrittura di un altro test TasksViewModel.

Passaggio 1. Scrivi il tuo test di ViewModel

Dovrai scrivere setFilterAllTasks_tasksAddViewVisible(). Questo test dovrebbe verificare che, se hai impostato il tipo di filtro in modo che mostri tutte le attività, che il pulsante Aggiungi attività sia visibile.

  1. Utilizzando addNewTask_setsNewTaskEvent() come riferimento, scrivi un test in TasksViewModelTest denominato setFilterAllTasks_tasksAddViewVisible() che imposti la modalità filtro su ALL_TASKS e dichiari che tasksAddViewVisibleLiveData è true.


Usa il codice qui sotto per iniziare.

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel

        // When the filter type is ALL_TASKS

        // Then the "Add task" action is visible
        
    }

Nota:

  • L'enumerazione di TasksFilterType per tutte le attività è ALL_TASKS.
  • La visibilità del pulsante per aggiungere un'attività dipende dalla tasksAddViewVisible. di LiveData
  1. Esegui il test.

Passaggio 2. Confronta il tuo test con la soluzione

Confronta la tua soluzione con la seguente.

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
    }

Verifica se:

  • Il tuo tasksViewModel viene creato utilizzando la stessa istruzione ApplicationProvider.getApplicationContext() di AndroidX.
  • Viene chiamato il metodo setFiltering, passando l'enumerazione del tipo di filtro ALL_TASKS.
  • Puoi verificare che il valore di tasksAddViewVisible sia true utilizzando il metodo getOrAwaitNextValue.

Passaggio 3. Aggiungere una regola @before

Nota che all'inizio di entrambi i test definisci un TasksViewModel.

TasksViewModelTest

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

Quando hai ripetuto il codice di configurazione per più test, puoi utilizzare l'annotazione @before per creare un metodo di configurazione e rimuovere il codice ripetuto. Dal momento che tutti questi test testeranno il TasksViewModel e saranno necessari un modello di visualizzazione, sposta questo codice in un blocco @Before.

  1. Crea una variabile di istanza lateinit denominata tasksViewModel|.
  2. Crea un metodo setupViewModel.
  3. Annotalo con @Before.
  4. Sposta il codice di creazione dell'istanza di un modello di visualizzazione in setupViewModel.

TasksViewModelTest

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }
  1. Esegui il codice.

Avviso

Non fare quanto segue, non inizializzare

tasksViewModel

e la relativa definizione:

val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

In questo modo, la stessa istanza verrà utilizzata per tutti i test. Si tratta di un comportamento da evitare perché ogni test deve avere una nuova istanza dell'argomento in fase di test (in questo caso, il ViewView).

Il codice finale per TasksViewModelTest dovrebbe essere simile al seguente codice.

TasksViewModelTest

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }


    @Test
    fun addNewTask_setsNewTaskEvent() {

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.awaitNextValue()
        assertThat(
            value?.getContentIfNotHandled(), (not(nullValue()))
        )
    }

    @Test
    fun getTasksAddViewVisible() {

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.awaitNextValue(), `is`(true))
    }
    
}

Fai clic qui per visualizzare una differenza tra il codice che hai iniziato e quello finale.

Per scaricare il codice per il codelab finito, puoi utilizzare il comando git riportato di seguito:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1


In alternativa, puoi scaricare il repository come file ZIP, decomprimerlo e aprirlo in Android Studio.

Scarica Zip

Questo codelab ha trattato:

  • Come eseguire test da Android Studio.
  • Differenza tra test locali (test) e test di strumentazione (androidTest).
  • Come scrivere test delle unità locali utilizzando JUnit e Hamcrest.
  • Configurazione di test ViewModel con la libreria di test di AndroidX.

Corso Udacity:

Documentazione per gli sviluppatori Android:

Video:

Altro:

Per i link ad altri codelab in questo corso, consulta la pagina di destinazione Advanced Android in Kotlin.