Nozioni di base per i test

Questo codelab fa parte del corso Advanced Android in Kotlin. Per ottenere il massimo valore da questo corso, ti consigliamo di seguire i codelab in sequenza, ma non è obbligatorio. Tutti i codelab del corso sono elencati nella pagina di destinazione dei codelab Advanced Android in Kotlin.

Introduzione

Quando hai implementato la prima funzionalità della tua prima app, probabilmente hai eseguito il codice per verificare che funzionasse come previsto. Hai eseguito un test, anche se manuale. Man mano che aggiungevi e aggiornavi le funzionalità, probabilmente continuavi a eseguire il codice e a verificarne il funzionamento. Ma farlo manualmente ogni volta è faticoso, soggetto a errori e non è scalabile.

I computer sono ottimi per la scalabilità e l'automazione. Pertanto, gli sviluppatori di aziende grandi e piccole scrivono test automatizzati, ovvero test eseguiti da software e che non richiedono di utilizzare manualmente l'app per verificare che il codice funzioni.

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

Questo primo codelab tratta le nozioni di base dei test su Android. Scriverai i tuoi primi test e imparerai a testare LiveData e ViewModel.

Cosa devi già sapere

Devi avere familiarità con:

Obiettivi didattici

Verranno trattati i seguenti argomenti:

  • Come scrivere ed eseguire test delle unità su Android
  • Come utilizzare lo sviluppo basato sui test
  • Come scegliere test strumentati e test locali

Imparerai a conoscere le seguenti librerie e i seguenti concetti di codice:

In questo lab proverai a:

  • Configura, esegui e interpreta i test locali e strumentati in Android.
  • Scrivi test delle unità in Android utilizzando JUnit4 e Hamcrest.
  • Scrivi test semplici LiveData e ViewModel.

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

Questa app è scritta in Kotlin, ha diverse schermate, utilizza i componenti Jetpack e segue l'architettura di una Guida all'architettura dell'app. Se impari a testare questa app, potrai testare anche le 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 la base di codice.

Passaggio 1: esegui l'app di esempio

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

  • Crea una nuova attività con il pulsante Azione fluttuante Più. Inserisci prima un titolo, poi aggiungi ulteriori informazioni sull'attività. Salvalo con il pulsante di azione rapida con il segno di spunta verde.
  • Nell'elenco delle attività, fai clic sul titolo dell'attività appena completata e guarda 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 delle attività, apri il menu del filtro e filtra le attività in base allo stato Attiva e Completata.
  • Apri il riquadro di navigazione a scomparsa e fai clic su Statistiche.
  • Torna alla schermata di panoramica e seleziona Cancella completate dal menu del riquadro di navigazione per eliminare tutte le attività con lo stato Completata.

Passaggio 2: esplora il codice dell'app di esempio

L'app TO-DO si basa sul popolare test e sull'esempio di architettura Architecture Blueprints (utilizzando la versione dell'esempio di architettura reattiva). L'app segue l'architettura di una Guida all'architettura delle app. Utilizza ViewModels con Fragment, un repository e Room. Se hai familiarità con uno degli esempi riportati di seguito, questa app ha un'architettura simile:

È più importante comprendere l'architettura generale dell'app che avere una conoscenza approfondita della logica di un livello specifico.

Ecco il riepilogo dei pacchetti che troverai:

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

.addedittask

La schermata Aggiungi o modifica un'attività:codice del livello UI per aggiungere o modificare un'attività.

.data

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

.statistics

Schermata delle statistiche:codice del livello UI per la schermata delle statistiche.

.taskdetail

Schermata dei dettagli dell'attività:codice del livello UI per una singola attività.

.tasks

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

.util

Classi di utilità:classi condivise utilizzate in varie parti dell'app, ad esempio per il layout di aggiornamento tramite scorrimento utilizzato su più schermate.

Livello dati (.data)

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

DefaultTasksRepository coordina o media tra il livello di rete e il livello di database ed è ciò che restituisce i dati al livello UI.

Livello UI ( .addedittask, .statistics, .taskdetail, .tasks)

Ciascuno dei pacchetti del livello UI contiene un fragment e un view model, insieme a qualsiasi altra classe necessaria per la UI (ad esempio un adattatore per l'elenco delle attività). L'TaskActivity è l'attività che contiene tutti i frammenti.

Navigazione

La navigazione per l'app è controllata dal componente di navigazione. È definito nel file nav_graph.xml. La navigazione viene attivata nei modelli di visualizzazione utilizzando la classe Event; i modelli di visualizzazione determinano anche quali argomenti passare. I fragment osservano le Event e si occupano della navigazione effettiva tra le schermate.

In questa attività, eseguirai i tuoi primi test.

  1. In Android Studio, apri il riquadro Project e individua queste tre cartelle:
  • 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 il codice sorgente della tua app. I set di origine, colorati di verde (androidTest e test), contengono i test. Quando crei un nuovo progetto Android, per impostazione predefinita vengono creati i seguenti tre insiemi di origini. Sono:

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

La differenza tra test locali e test strumentati sta nel modo in cui vengono eseguiti.

Test locali (test set di origini)

Questi test vengono eseguiti localmente sulla JVM della macchina di sviluppo e non richiedono un emulatore o un dispositivo fisico. Per questo motivo, sono veloci, ma la loro fedeltà è inferiore, il che significa che si comportano in modo meno realistico.

In Android Studio, i test locali sono rappresentati da un'icona a forma di triangolo verde e rosso.

Test strumentati (set di origini androidTest)

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

In Android Studio, i test strumentati sono rappresentati da un Android con un'icona a forma di triangolo verde e rosso.

Passaggio 1: esegui un test locale

  1. Apri la cartella test fino a trovare il file ExampleUnitTest.kt.
  2. Fai clic con il tasto destro del mouse e seleziona Esegui ExampleUnitTest.

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

  1. Nota i segni di spunta verdi ed espandi i risultati del test per verificare che un test chiamato addition_isCorrect sia stato superato. È bello sapere che l'addizione funziona come previsto.

Passaggio 2: fai in modo che il test non vada a buon fine

Di seguito è riportato il test che hai appena 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)
   }
}

Tieni presente che i test

  • sono una classe in uno dei set di origini di test.
  • contengono funzioni che iniziano con l'annotazione @Test (ogni funzione è un singolo test).
  • contengono in genere istruzioni di asserzione.

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

Un'asserzione è il nucleo del test. Si tratta di un'istruzione di codice che verifica che il codice o l'app si siano comportati come previsto. In questo caso, l'asserzione è assertEquals(4, 2 + 2), che verifica che 4 sia uguale a 2 + 2.

Per vedere l'aspetto di un test non riuscito, aggiungi un'asserzione che dovrebbe non riuscire. Verificherà che 3 sia 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 del test, nota una X accanto al test.

  1. Tieni presente anche:
  • Una singola asserzione non riuscita causa l'esito negativo dell'intero test.
  • Ti viene comunicato il valore previsto (3) rispetto al valore effettivamente calcolato (2).
  • Viene visualizzata la riga dell'asserzione non riuscita (ExampleUnitTest.kt:16).

Passaggio 3: esegui un test strumentato

I test strumentati si trovano nel set di origine androidTest.

  1. Apri il set di origini androidTest.
  2. Esegui il test denominato 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 seguente uno smartphone Pixel 2 emulato):

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

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

Passaggio 1: crea una classe di prova

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

StatisticsUtils.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 che contiene due numeri: la percentuale di attività completate e la percentuale di attività attive.

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

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

Si apre la finestra di dialogo Crea test:

  1. Modifica il Nome del corso in StatisticsUtilsTest (anziché StatisticsUtilsKtTest; è leggermente meglio non avere KT nel nome del corso di test).
  2. Mantieni i valori predefiniti per le altre impostazioni. JUnit 4 è la libreria di test appropriata. Il pacchetto di destinazione è corretto (rispecchia la posizione della classe StatisticsUtils) e non devi selezionare nessuna delle caselle di controllo (in questo modo viene generato solo codice aggiuntivo, ma scriverai il test da zero).
  3. Premi OK.

Si apre la finestra di dialogo Scegli directory di destinazione:

Esegui un test locale perché la tua funzione esegue calcoli matematici e non include codice specifico per Android. Pertanto, non è necessario eseguirlo su un dispositivo reale o emulato.

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

Passaggio 2: scrivi la tua prima funzione di test

Scriverai un test che controlla:

  • se non ci sono attività completate e una 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.

StatisticsUtilsTest.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 per queste attività.
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. Verifica che result sia quello che ti aspettavi, utilizzando le asserzioni.
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

Ecco il codice completo.

StatisticsUtilsTest.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 tasto destro del mouse su StatisticsUtilsTest e seleziona Esegui).

Deve superare:

Passaggio 3: aggiungi la dipendenza Hamcrest

Poiché i test fungono da documentazione di ciò che fa il codice, è utile che siano leggibili. Confronta le due seguenti affermazioni:

assertEquals(result.completedTasksPercent, 0f)

// versus

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

La seconda asserzione sembra molto più una frase umana. È scritto utilizzando un framework di asserzioni chiamato Hamcrest. Un altro strumento utile per scrivere asserzioni leggibili è la libreria Truth. In questo codelab utilizzerai Hamcrest per scrivere asserzioni.

  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, quando aggiungi una dipendenza utilizzi implementation, ma qui stai utilizzando testImplementation. Quando è tutto pronto per condividere la tua app con il mondo, è meglio non aumentare le dimensioni dell'APK con il codice di test o le dipendenze dell'app. Puoi specificare 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 fonti, inclusi quelli di test.
  • testImplementation: la dipendenza è disponibile solo nel set 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 nel set di origini di test. Inoltre, garantisce che Hamcrest non venga incluso nell'app finale.

Passaggio 4: utilizza Hamcrest per scrivere asserzioni

  1. Aggiorna il test getActiveAndCompletedStats_noCompleted_returnsHundredZero() in modo che utilizzi 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))

Se richiesto, puoi utilizzare l'importazione import org.hamcrest.Matchers.`is`.

Il test finale avrà un aspetto simile al codice riportato di seguito.

StatisticsUtilsTest.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 insegnerà tutti i dettagli di Hamcrest, quindi se vuoi saperne di più, consulta il tutorial ufficiale.

Si tratta di un'attività facoltativa per fare pratica.

In questa attività, scriverai altri test utilizzando JUnit e Hamcrest. Scriverai anche test utilizzando una strategia derivata dalla pratica di programmazione del Test Driven Development. Lo sviluppo basato sui test o TDD è una scuola di pensiero di programmazione che afferma che, invece di scrivere prima il codice della funzionalità, si scrivono prima i test. Poi scrivi il codice della funzionalità con l'obiettivo di superare i test.

Passaggio 1: Scrivi i test

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

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

Passaggio 2: Scrivere un test per un bug

Il codice per getActiveAndCompletedStats così come è scritto presenta un bug. Nota come non gestisce correttamente cosa succede se l'elenco è vuoto o null. 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, utilizzerai lo sviluppo basato sui test. Lo sviluppo basato sui test segue questi passaggi.

  1. Scrivi il test utilizzando la struttura Given, When, Then e con un nome che segue la convenzione.
  2. Conferma che il test non è riuscito.
  3. Scrivi il codice minimo per superare il test.
  4. Ripeti l'operazione per tutti i test.

Invece di iniziare correggendo il bug, inizierai scrivendo i test. In questo modo, potrai confermare di avere test che ti proteggono dal reintrodurre accidentalmente 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 e entrambe le percentuali devono essere 0.
  3. Esegui i test e verifica che non vadano a buon fine:

Passaggio 3: Correggi il bug

Ora che hai i test, 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 di nuovo i test e verifica che ora vengano superati tutti.

Seguendo il TDD e scrivendo prima i test, hai contribuito a garantire che:

  • Le nuove funzionalità hanno sempre test associati, quindi i test fungono da documentazione di ciò che fa il codice.
  • I test verificano i risultati corretti e proteggono dai bug che hai già riscontrato.

Soluzione: scrivere più test

Ecco tutti i test e il codice della funzionalità corrispondente.

StatisticsUtilsTest.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))
    }
}

StatisticsUtils.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 basi della scrittura e dell'esecuzione dei test. Successivamente, imparerai a scrivere test ViewModel e LiveData di base.

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

Inizia scrivendo i test per TasksViewModel.


Ti concentrerai sui test la cui logica è interamente contenuta nel view model e che non si basano sul codice del repository. Il codice del repository include codice asincrono, database e chiamate di rete, che aumentano la complessità dei test. Per il momento eviterai di farlo e ti concentrerai sulla scrittura di test per la funzionalità ViewModel che non testa direttamente nulla nel repository.



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

TasksViewModel.kt

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

Passaggio 1: Crea una classe TasksViewModelTest

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

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

  1. Nella schermata Crea test, fai clic su Ok per accettare (non è necessario modificare le impostazioni predefinite).
  2. Nella finestra di dialogo Scegli directory di destinazione, scegli la directory test.

Passaggio 2: Inizia a scrivere il test del ViewModel

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

  1. Crea un nuovo test denominato 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

    }
    
}

Che cos'è il contesto dell'applicazione?

Quando crei un'istanza di TasksViewModel per il test, il relativo costruttore richiede un contesto dell'applicazione. Tuttavia, in questo test non stai creando un'applicazione completa con attività, UI e frammenti, quindi come fai a ottenere un contesto dell'applicazione?

TasksViewModelTest.kt

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

Le librerie di test AndroidX includono classi e metodi che forniscono versioni di componenti come applicazioni e attività destinate ai test. Quando esegui un test locale in cui sono necessarie classi del framework Android simulate(ad esempio un contesto dell'applicazione), segui questi passaggi per configurare correttamente AndroidX Test:

  1. Aggiungi le dipendenze principali ed ext di AndroidX Test
  2. Aggiungi la dipendenza della libreria di test Robolectric
  3. Annota la classe con l'esecutore del test AndroidJUnit4
  4. Scrivere codice di test AndroidX

Completerai questi passaggi e poi capirai cosa fanno insieme.

Passaggio 3: Aggiungi le dipendenze di Gradle

  1. Copia queste dipendenze nel file build.gradle del modulo dell'app per aggiungere le dipendenze principali di AndroidX Test e ext, nonché la dipendenza di test Robolectric.

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: Aggiungere JUnit Test Runner

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

TasksViewModelTest.kt

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

Passaggio 5: Utilizzare AndroidX Test

A questo punto, puoi utilizzare la libreria AndroidX Test. È incluso il metodo ApplicationProvider.getApplicationContext, che recupera un contesto dell'applicazione.

  1. Crea un TasksViewModel utilizzando ApplicationProvider.getApplicationContext() dalla libreria di test 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 codice riportato di seguito.

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 verificare 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 forniscono versioni di componenti come Applicazioni e Attività, destinate ai test. Ad esempio, questo codice che hai scritto è un esempio di funzione di test AndroidX per ottenere un contesto dell'applicazione.

ApplicationProvider.getApplicationContext()

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

  • Puoi eseguire lo stesso test come test locale o test strumentato.
  • Non devi imparare API di test diverse per i test locali e quelli strumentati.

Ad esempio, poiché hai scritto il codice utilizzando le librerie di test AndroidX, puoi spostare la classe TasksViewModelTest dalla cartella test alla cartella androidTest e i test continueranno a essere eseguiti. getApplicationContext() funziona in modo leggermente diverso a seconda che venga eseguito come test locale o strumentato:

  • Se si tratta di un test strumentato, riceverà il contesto dell'applicazione effettivo fornito all'avvio di un emulatore o alla connessione a 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ù rapidamente dell'avvio di un emulatore o dell'esecuzione su un dispositivo. Senza la dipendenza Robolectric, riceverai questo errore:

Che cosa fa @RunWith(AndroidJUnit4::class)?

Un test runner è un componente JUnit che esegue i test. Senza un test runner, i test non verrebbero eseguiti. JUnit fornisce un test runner predefinito che viene scaricato automaticamente. @RunWith sostituisce il test runner predefinito.

Il runner di test AndroidJUnit4 consente a AndroidX Test di eseguire il test in modo diverso a seconda che si tratti di test strumentati o locali.

Passaggio 6: Correggere gli avvisi di Robolectric

Quando esegui il codice, nota che viene utilizzato Robolectric.

Grazie ad AndroidX Test e al runner di test AndroidJUnit4, questa operazione viene eseguita 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 manifest Android corretto. L'opzione includeAndroidResources ti consente di accedere alle risorse Android nei test unitari, 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ù complesso. L'esecuzione di test su Android Q richiede Java 9. Anziché tentare di configurare Android Studio per utilizzare Java 9, per questo codelab mantieni l'SDK di compilazione e di destinazione a 28.

In sintesi:

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

Congratulazioni, stai utilizzando sia la libreria di test AndroidX sia Robolectric per eseguire un test. Il test non è terminato (non hai ancora scritto un'istruzione assert, c'è solo scritto // TODO test LiveData). Imparerai a scrivere istruzioni assert con LiveData.

In questa attività imparerai come asserire correttamente il valore di LiveData.

Ecco da dove avevi interrotto senza il test del modello di visualizzazione addNewTask_setsNewTaskEvent.

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 fare due cose:

  1. Utilizza InstantTaskExecutorRule
  2. Garantire l'osservazione LiveData

Passaggio 1: Utilizzare InstantTaskExecutorRule

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

Questa regola esegue tutti i job in background correlati 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 il test di LiveData, utilizza questa regola.

  1. Aggiungi la dipendenza Gradle per la libreria di test principale di Architecture Components (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 nel verificare che la LiveData che stai testando venga osservata.

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

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

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

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

Questo pone un problema: nel test TasksViewModel non hai un'attività o un fragment per osservare LiveData. Per ovviare a questo problema, puoi utilizzare il metodo observeForever, che garantisce l'osservazione costante di LiveData, senza la necessità di un LifecycleOwner. Quando observeForever, devi ricordarti di rimuovere l'osservatore per evitare una fuga di osservatori.

Il risultato sarà simile al codice riportato di seguito. 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)
    }
}

È un sacco di codice boilerplate per osservare un singolo LiveData in un test. Esistono alcuni modi per eliminare questo testo standard. Creerai una funzione di estensione chiamata LiveDataTestUtil per semplificare l'aggiunta di osservatori.

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


  1. Copia e incolla il codice riportato di seguito.

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
}

Questo è un metodo piuttosto complicato. Crea una funzione di estensione Kotlin chiamata getOrAwaitValue che aggiunge un osservatore, recupera il valore LiveData e poi pulisce l'osservatore. In pratica, una versione breve e riutilizzabile del codice observeForever mostrato sopra. Per una spiegazione completa di questa classe, consulta questo post del blog.

Passaggio 3: Utilizza getOrAwaitValue per scrivere l'asserzione

In questo passaggio, utilizzi il metodo getOrAwaitValue e scrivi un'istruzione assert che verifica che newTaskEvent sia stato attivato.

  1. Ottieni il valore LiveData per newTaskEvent utilizzando getOrAwaitValue.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. Afferma che il valore non è null.
assertThat(value.getContentIfNotHandled(), (not(nullValue())))

Il test completo dovrebbe essere simile al codice riportato di seguito.

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 test superato.

Ora che hai visto come scrivere un test, scrivine uno tu. In questo passaggio, utilizzando le competenze che hai acquisito, esercitati a scrivere un altro test TasksViewModel.

Passaggio 1: Scrivere il proprio test ViewModel

Scriverai setFilterAllTasks_tasksAddViewVisible(). Questo test deve verificare che, se hai impostato il tipo di filtro in modo da mostrare tutte le attività, il pulsante Aggiungi attività sia visibile.

  1. Utilizzando addNewTask_setsNewTaskEvent() come riferimento, scrivi un test in TasksViewModelTest chiamato setFilterAllTasks_tasksAddViewVisible() che imposta la modalità di filtro su ALL_TASKS e verifica che LiveData tasksAddViewVisible sia true.


Utilizza il codice riportato di seguito 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 TasksFilterType per tutte le attività è ALL_TASKS.
  • La visibilità del pulsante per aggiungere un'attività è controllata da LiveData tasksAddViewVisible.
  1. Esegui il test.

Passaggio 2: Confronta il tuo test con la soluzione

Confronta la tua soluzione con quella riportata di seguito.

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

Controlla se:

  • Crei il tuo tasksViewModel utilizzando la stessa istruzione AndroidX ApplicationProvider.getApplicationContext().
  • Chiami il metodo setFiltering, passando l'enumerazione del tipo di filtro ALL_TASKS.
  • Verifica che tasksAddViewVisible sia true utilizzando il metodo getOrAwaitNextValue.

Passaggio 3: Aggiungere una regola @Before

Nota come all'inizio di entrambi i test viene definito un TasksViewModel.

TasksViewModelTest

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

Quando hai un codice di configurazione ripetuto per più test, puoi utilizzare l'annotazione @Before per creare un metodo di configurazione e rimuovere il codice ripetuto. Poiché tutti questi test verificheranno TasksViewModel e richiedono un view model, sposta questo codice in un blocco @Before.

  1. Crea una variabile di istanza lateinit denominata tasksViewModel|.
  2. Crea un metodo denominato setupViewModel.
  3. Annotalo con @Before.
  4. Sposta il codice di creazione del 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 eseguire le seguenti operazioni, non inizializzare

tasksViewModel

con la relativa definizione:

val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

In questo modo, verrà utilizzata la stessa istanza per tutti i test. Si tratta di un aspetto da evitare perché ogni test deve avere un'istanza nuova del soggetto in esame (in questo caso, il ViewModel).

Il codice finale per TasksViewModelTest dovrebbe avere l'aspetto del codice riportato di seguito.

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 iniziale e quello finale.

Per scaricare il codice del codelab completato, 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 i seguenti argomenti:

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

Corso Udacity:

Documentazione per sviluppatori Android:

Video:

Altro:

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