Introduzione ai test double e all'iniezione delle dipendenze

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

Questo secondo codelab di test riguarda i test doppi: quando utilizzarli in Android e come implementarli utilizzando l'inserimento delle dipendenze, il pattern Service Locator e le librerie. In questo modo, imparerai a scrivere:

  • Test delle unità del repository
  • Test di integrazione di fragment e ViewModel
  • Test di navigazione dei fragment

Cosa devi già sapere

Devi avere familiarità con:

Obiettivi didattici

  • Come pianificare una strategia di test
  • Come creare e utilizzare i test double, ovvero i fake e i mock
  • Come utilizzare l'inserimento manuale delle dipendenze su Android per i test unitari e di integrazione
  • Come applicare il pattern Service Locator
  • Come testare repository, frammenti, modelli di visualizzazione e il componente di navigazione

Utilizzerai le seguenti librerie e i seguenti concetti di codice:

In questo lab proverai a:

  • Scrivere test delle unità per un repository utilizzando un test double e l'inserimento delle dipendenze.
  • Scrivi test delle unità per un view model utilizzando un test double e l'inserimento delle dipendenze.
  • Scrivi test di integrazione per i fragment e i relativi view model utilizzando il framework di test UI Espresso.
  • Scrivi test di navigazione utilizzando Mockito ed Espresso.

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 alcune schermate, utilizza i componenti Jetpack e segue l'architettura di una Guida all'architettura delle app. Se impari a testare questa app, potrai testare anche le app che utilizzano le stesse librerie e la stessa architettura.

Scarica il codice

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 end_codelab_1

Prenditi un momento per acquisire familiarità con il codice seguendo le istruzioni riportate di seguito.

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 questo codelab, imparerai a testare repository, visualizzare modelli e frammenti utilizzando test doppi e l'inserimento delle dipendenze. Prima di scoprire quali sono, è importante capire il ragionamento che guiderà cosa e come scriverai questi test.

Questa sezione illustra alcune best practice per i test in generale, così come si applicano ad Android.

La piramide dei test

Quando pensi a una strategia di test, ci sono tre aspetti correlati:

  • Ambito: quanto codice viene toccato dal test? I test possono essere eseguiti su un singolo metodo, sull'intera applicazione o su una parte di essa.
  • Velocità: quanto velocemente viene eseguito il test? Le velocità di test possono variare da millisecondi a diversi minuti.
  • Fedeltà: quanto è "reale" il test? Ad esempio, se parte del codice che stai testando deve effettuare una richiesta di rete, il codice di test effettua questa richiesta di rete o simula il risultato? Se il test comunica effettivamente con la rete, significa che ha una fedeltà più elevata. Il compromesso è che il test potrebbe richiedere più tempo per essere eseguito, potrebbe generare errori se la rete non è disponibile o potrebbe essere costoso da utilizzare.

Esistono compromessi intrinseci tra questi aspetti. Ad esempio, velocità e fedeltà sono un compromesso: più veloce è il test, generalmente, minore è la fedeltà e viceversa. Un modo comune per dividere i test automatici è in queste tre categorie:

  • Test delle unità: si tratta di test molto mirati che vengono eseguiti su una singola classe, in genere un singolo metodo in quella classe. Se un test unitario non va a buon fine, puoi sapere esattamente in quale punto del codice si trova il problema. Hanno una bassa fedeltà perché nel mondo reale la tua app comporta molto di più dell'esecuzione di un metodo o di una classe. Sono abbastanza veloci da essere eseguiti ogni volta che modifichi il codice. Il più delle volte si tratta di test eseguiti localmente (nel set di origini test). Esempio: test di singoli metodi in modelli di visualizzazione e repository.
  • Test di integrazione: testano l'interazione di diverse classi per assicurarsi che si comportino come previsto quando vengono utilizzate insieme. Un modo per strutturare i test di integrazione è quello di testare una singola funzionalità, ad esempio la possibilità di salvare un'attività. Testano un ambito di codice più ampio rispetto ai test delle unità, ma sono comunque ottimizzati per essere eseguiti rapidamente, anziché con la massima fedeltà. Possono essere eseguiti localmente o come test di strumentazione, a seconda della situazione. Esempio: test di tutte le funzionalità di una singola coppia di frammenti e modelli di visualizzazione.
  • Test end-to-end (E2e): testa una combinazione di funzionalità che funzionano insieme. Testano ampie porzioni dell'app, simulano da vicino l'utilizzo reale e pertanto sono in genere lenti. Hanno la massima fedeltà e ti dicono che la tua applicazione funziona effettivamente nel suo complesso. In generale, questi test saranno test strumentati (nel set di origini androidTest)
    Esempio: avvio dell'intera app e test di alcune funzionalità insieme.

La proporzione suggerita di questi test è spesso rappresentata da una piramide, in cui la maggior parte dei test sono test delle unità.

Architettura e test

La tua capacità di testare l'app a tutti i diversi livelli della piramide di test è intrinsecamente legata all'architettura dell'app. Ad esempio, un'applicazione con un'architettura estremamente scadente potrebbe inserire tutta la sua logica all'interno di un unico metodo. Potresti essere in grado di scrivere un test end-to-end per questo, poiché questi test tendono a testare grandi porzioni dell'app, ma cosa dire della scrittura di test delle unità o di integrazione? Con tutto il codice in un unico posto, è difficile testare solo il codice relativo a una singola unità o funzionalità.

Un approccio migliore sarebbe quello di suddividere la logica dell'applicazione in più metodi e classi, consentendo di testare ogni parte in modo isolato. L'architettura è un modo per dividere e organizzare il codice, il che consente di eseguire più facilmente test delle unità e di integrazione. L'app TO-DO che testerai segue un'architettura particolare:



In questa lezione, vedrai come testare parti dell'architettura precedente, in modo isolato:

  1. Per prima cosa, esegui il test unitario del repository.
  2. Poi utilizzerai un test double nel modello di visualizzazione, necessario per il test delle unità e il test di integrazione del modello di visualizzazione.
  3. Successivamente, imparerai a scrivere test di integrazione per fragment e i relativi modelli di visualizzazione.
  4. Infine, imparerai a scrivere test di integrazione che includono il componente Navigation.

Il test end-to-end verrà trattato nella prossima lezione.

Quando scrivi un test delle unità per una parte di una classe (un metodo o una piccola raccolta di metodi), il tuo obiettivo è testare solo il codice in quella classe.

Testare solo il codice in una o più classi specifiche può essere complicato. Ecco un esempio. Apri il corso data.source.DefaultTaskRepository nel set di origini main. Questo è il repository dell'app ed è la classe per cui scriverai i test delle unità in seguito.

Il tuo obiettivo è testare solo il codice in quella classe. Tuttavia, DefaultTaskRepository dipende da altre classi, come LocalTaskDataSource e RemoteTaskDataSource, per funzionare. In altre parole, LocalTaskDataSource e RemoteTaskDataSource sono dipendenze di DefaultTaskRepository.

Pertanto, ogni metodo in DefaultTaskRepository chiama metodi nelle classi di origine dati, che a loro volta chiamano metodi in altre classi per salvare le informazioni in un database o comunicare con la rete.



Ad esempio, dai un'occhiata a questo metodo in DefaultTasksRepo.

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks è una delle chiamate più "base" che puoi effettuare al tuo repository. Questo metodo include la lettura da un database SQLite e l'esecuzione di chiamate di rete (la chiamata a updateTasksFromRemoteDataSource). Ciò comporta molto più codice rispetto al solo codice del repository.

Ecco alcuni motivi più specifici per cui è difficile testare il repository:

  • Devi occuparti della creazione e della gestione di un database per eseguire anche i test più semplici per questo repository. Ciò solleva domande come "deve essere un test locale o strumentato?" e se devi utilizzare AndroidX Test per ottenere un ambiente Android simulato.
  • Alcune parti del codice, come il codice di rete, possono richiedere molto tempo per l'esecuzione o occasionalmente anche non riuscire, creando test a esecuzione prolungata e instabili.
  • I test potrebbero perdere la capacità di diagnosticare quale codice è responsabile di un errore del test. I test potrebbero iniziare a testare il codice non del repository, quindi, ad esempio, i test delle unità del presunto "repository" potrebbero non riuscire a causa di un problema in parte del codice dipendente, ad esempio il codice del database.

Test doubles

La soluzione è che, quando testi il repository, non devi utilizzare il codice di rete o del database reale, ma un test double. Un test double è una versione di una classe creata appositamente per i test. È pensata per sostituire la versione reale di un corso nei test. È simile a come una controfigura è un attore specializzato in acrobazie e sostituisce l'attore reale per le azioni pericolose.

Ecco alcuni tipi di test double:

Falso

Un test double che ha un'implementazione"funzionante" della classe, ma è implementato in modo da renderlo adatto ai test ma non alla produzione.

Mock

Un test double che tiene traccia dei metodi chiamati. Supera o meno un test a seconda che i suoi metodi siano stati chiamati correttamente.

Stub

Un test double che non include alcuna logica e restituisce solo ciò che programmi. Un StubTaskRepository potrebbe essere programmato per restituire determinate combinazioni di attività da getTasks, ad esempio.

Dummy

Un test double che viene passato ma non utilizzato, ad esempio se devi solo fornirlo come parametro. Se avessi un NoOpTaskRepository, implementerebbe semplicemente TaskRepository senza codice in nessuno dei metodi.

Spy

Un test double che tiene traccia anche di alcune informazioni aggiuntive. Ad esempio, se hai creato un SpyTaskRepository, potrebbe tenere traccia del numero di volte in cui è stato chiamato il metodo addTask.

Per saperne di più sui test doppi, consulta l'articolo Testing on the Toilet: Know Your Test Doubles.

I test double più comuni utilizzati in Android sono Fakes e Mocks.

In questa attività, creerai un test double FakeDataSource per il test unitario di DefaultTasksRepository disaccoppiato dalle origini dati effettive.

Passaggio 1: crea la classe FakeDataSource

In questo passaggio creerai una classe denominata FakeDataSouce, che sarà un test double di LocalDataSource e RemoteDataSource.

  1. Nel set di origini test, fai clic con il tasto destro del mouse e seleziona Nuovo > Pacchetto.

  1. Crea un pacchetto data con un pacchetto source all'interno.
  2. Crea una nuova classe denominata FakeDataSource nel pacchetto data/source.

Passaggio 2: implementa l'interfaccia TasksDataSource

Per poter utilizzare la nuova classe FakeDataSource come test double, deve essere in grado di sostituire le altre origini dati. Queste origini dati sono TasksLocalDataSource e TasksRemoteDataSource.

  1. Nota come entrambi implementano l'interfaccia TasksDataSource.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. Fai in modo che FakeDataSource implementi TasksDataSource:
class FakeDataSource : TasksDataSource {

}

Android Studio segnalerà che non hai implementato i metodi richiesti per TasksDataSource.

  1. Utilizza il menu di correzione rapida e seleziona Implementa membri.


  1. Seleziona tutti i metodi e premi Ok.

Passaggio 3: implementa il metodo getTasks in FakeDataSource

FakeDataSource è un tipo specifico di test double chiamato fake. Un fake è un test double che ha un'implementazione"funzionante" della classe, ma è implementato in modo da renderlo adatto ai test ma non alla produzione. L'implementazione "Funzionante" indica che la classe produrrà output realistici in base agli input.

Ad esempio, l'origine dati fittizia non si connetterà alla rete né salverà nulla in un database, ma utilizzerà solo un elenco in memoria. In questo modo "funzionerà come previsto", nel senso che i metodi per ottenere o salvare le attività restituiranno i risultati previsti, ma non potrai mai utilizzare questa implementazione in produzione, perché non viene salvata sul server o in un database.

Un FakeDataSource

  • consente di testare il codice in DefaultTasksRepository senza dover fare affidamento su un database o una rete reali.
  • fornisce un'implementazione "abbastanza reale" per i test.
  1. Modifica il costruttore FakeDataSource per creare un var chiamato tasks che sia un MutableList<Task>? con un valore predefinito di un elenco modificabile vuoto.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


Questo è l'elenco delle attività che "simulano" una risposta del database o del server. Per ora, l'obiettivo è testare il metodo getTasks del repository. Vengono chiamati i metodi getTasks, deleteAllTasks e saveTask dell' origine dati.

Scrivi una versione fittizia di questi metodi:

  1. Scrivi getTasks: se tasks non è null, restituisci un risultato Success. Se tasks è null, restituisci un risultato Error.
  2. Scrivi deleteAllTasks: cancella l'elenco delle attività modificabili.
  3. Scrivi saveTask: aggiungi l'attività all'elenco.

Questi metodi, implementati per FakeDataSource, hanno l'aspetto del codice riportato di seguito.

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

Ecco le istruzioni di importazione, se necessarie:

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

Questo è simile al funzionamento delle origini dati locali e remote effettive.

In questo passaggio, utilizzerai una tecnica chiamata inserimento manuale delle dipendenze per poter utilizzare il test double fittizio che hai appena creato.

Il problema principale è che hai un FakeDataSource, ma non è chiaro come lo utilizzi nei test. Deve sostituire TasksRemoteDataSource e TasksLocalDataSource, ma solo nei test. TasksRemoteDataSource e TasksLocalDataSource sono entrambi dipendenze di DefaultTasksRepository, il che significa che DefaultTasksRepositories richiede o "dipende" da queste classi per essere eseguito.

Al momento, le dipendenze vengono create all'interno del metodo init di DefaultTasksRepository.

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

Poiché stai creando e assegnando taskLocalDataSource e tasksRemoteDataSource all'interno di DefaultTasksRepository, sono essenzialmente codificati in modo permanente. Non è possibile sostituire il tuo stuntman.

Quello che devi fare è fornire queste origini dati alla classe, anziché codificarle in modo permanente. La fornitura di dipendenze è nota come inserimento delle dipendenze. Esistono diversi modi per fornire le dipendenze e, di conseguenza, diversi tipi di iniezione delle dipendenze.

L'inserimento delle dipendenze del costruttore consente di sostituire il test double passandolo al costruttore.

Nessun inserimento

Iniezione

Passaggio 1: utilizza l'inserimento delle dipendenze del costruttore in DefaultTasksRepository

  1. Modifica il costruttore di DefaultTaskRepository in modo che accetti sia le origini dati che il dispatcher delle coroutine (che dovrai scambiare anche per i test, come descritto in dettaglio nella sezione della terza lezione sulle coroutine).Application

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. Poiché hai passato le dipendenze, rimuovi il metodo init. Non è più necessario creare le dipendenze.
  2. Elimina anche le vecchie variabili dell'istanza. Li stai definendo nel costruttore:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Infine, aggiorna il metodo getRepository per utilizzare il nuovo costruttore:

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

Ora stai utilizzando l'inserimento delle dipendenze del costruttore.

Passaggio 2: utilizza FakeDataSource nei test

Ora che il codice utilizza l'inserimento delle dipendenze del costruttore, puoi utilizzare l'origine dati fittizia per testare DefaultTasksRepository.

  1. Fai clic con il tasto destro del mouse sul nome della classe DefaultTasksRepository e seleziona Genera, poi Testa.
  2. Segui le istruzioni per creare DefaultTasksRepositoryTest nel set di origini test.
  3. Nella parte superiore della nuova classe DefaultTasksRepositoryTest, aggiungi le variabili membro riportate di seguito per rappresentare i dati nelle origini dati fittizie.

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. Crea tre variabili: due variabili membro FakeDataSource (una per ogni origine dati del repository) e una variabile per DefaultTasksRepository che testerai.

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Crea un metodo per configurare e inizializzare un DefaultTasksRepository testabile. Questo DefaultTasksRepository utilizzerà il tuo test double, FakeDataSource.

  1. Crea un metodo denominato createRepository e annotalo con @Before.
  2. Crea un'istanza delle origini dati fittizie utilizzando gli elenchi remoteTasks e localTasks.
  3. Crea un'istanza di tasksRepository utilizzando le due origini dati fittizie che hai appena creato e Dispatchers.Unconfined.

Il metodo finale dovrebbe essere simile al codice riportato di seguito.

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

Passaggio 3: scrivi il test DefaultTasksRepository getTasks()

È ora di scrivere un test DefaultTasksRepository.

  1. Scrivi un test per il metodo getTasks del repository. Verifica che quando chiami getTasks con true (il che significa che deve ricaricare dall'origine dati remota), restituisca i dati dall'origine dati remota (anziché dall'origine dati locale).

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

Riceverai un errore quando chiami getTasks:

Passaggio 4: aggiungi runBlockingTest

L'errore della coroutine è previsto perché getTasks è una funzione suspend e devi avviare una coroutine per chiamarla. Per farlo, devi avere un ambito di coroutine. Per risolvere questo errore, dovrai aggiungere alcune dipendenze Gradle per la gestione dell'avvio delle coroutine nei test.

  1. Aggiungi le dipendenze richieste per testare le coroutine al set di origini di test utilizzando testImplementation.

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

Non dimenticare di sincronizzare.

kotlinx-coroutines-test è la libreria di test delle coroutine, pensata appositamente per testarle. Per eseguire i test, utilizza la funzione runBlockingTest. Si tratta di una funzione fornita dalla libreria di test delle coroutine. Accetta un blocco di codice e lo esegue in un contesto di coroutine speciale che viene eseguito in modo sincrono e immediato, il che significa che le azioni si verificano in un ordine deterministico. In questo modo, le coroutine vengono eseguite come non coroutine, quindi è pensato per testare il codice.

Utilizza runBlockingTest nelle classi di test quando chiami una funzione suspend. Scoprirai di più sul funzionamento di runBlockingTest e su come testare le coroutine nel prossimo codelab di questa serie.

  1. Aggiungi @ExperimentalCoroutinesApi sopra la classe. Ciò indica che sai di utilizzare un'API coroutine sperimentale (runBlockingTest) nella classe. In caso contrario, riceverai un avviso.
  2. Torna a DefaultTasksRepositoryTest e aggiungi runBlockingTest in modo che includa l'intero test come "blocco" di codice

Questo test finale ha l'aspetto del codice riportato di seguito.

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. Esegui il nuovo test getTasks_requestsAllTasksFromRemoteDataSource e verifica che funzioni e che l'errore non sia più presente.

Hai appena visto come testare un repository a livello di unità. Nei passaggi successivi, utilizzerai di nuovo l'inserimento delle dipendenze e creerai un altro test double, questa volta per mostrare come scrivere test unitari e di integrazione per i tuoi view model.

I test delle unità devono testare solo la classe o il metodo che ti interessa. Questo è noto come test in isolamento, in cui isoli chiaramente la "unità" e testi solo il codice che fa parte di quell'unità.

Pertanto, TasksViewModelTest deve testare solo il codice TasksViewModel, non le classi di database, di rete o di repository. Pertanto, per i tuoi modelli di visualizzazione, proprio come hai fatto per il repository, creerai un repository fittizio e applicherai l'inserimento delle dipendenze per utilizzarlo nei test.

In questa attività, applichi l'inserimento delle dipendenze ai modelli di visualizzazione.

Passaggio 1: Crea un'interfaccia TasksRepository

Il primo passo per utilizzare l'inserimento delle dipendenze del costruttore è creare un'interfaccia comune condivisa tra la classe fittizia e quella reale.

Come si traduce tutto questo in pratica? Guarda TasksRemoteDataSource, TasksLocalDataSource e FakeDataSource e nota che condividono tutti la stessa interfaccia: TasksDataSource. In questo modo, puoi specificare nel costruttore di DefaultTasksRepository che accetti un TasksDataSource.

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

È questo che ci consente di inserire il tuo FakeDataSource.

Successivamente, crea un'interfaccia per DefaultTasksRepository, come hai fatto per le origini dati. Deve includere tutti i metodi pubblici (superficie API pubblica) di DefaultTasksRepository.

  1. Apri DefaultTasksRepository e fai clic con il tasto destro del mouse sul nome del corso. Quindi seleziona Refactor -> Extract -> Interface.

  1. Scegli Estrai in un file separato.

  1. Nella finestra Estrai interfaccia, modifica il nome dell'interfaccia in TasksRepository.
  2. Nella sezione Interfaccia Membri da formare, seleziona tutti i membri tranne i due membri complementari e i metodi privati.


  1. Fai clic su Refactor. La nuova interfaccia TasksRepository dovrebbe essere visualizzata nel pacchetto data/source .

e DefaultTasksRepository ora implementa TasksRepository.

  1. Esegui l'app (non i test) per assicurarti che tutto funzioni ancora correttamente.

Passaggio 2: Crea FakeTestRepository

Ora che hai l'interfaccia, puoi creare il test double DefaultTaskRepository.

  1. Nel set di origini test, in data/source crea il file Kotlin e la classe FakeTestRepository.kt ed estendili dall'interfaccia TasksRepository.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Ti verrà comunicato che devi implementare i metodi dell'interfaccia.

  1. Passa il mouse sopra l'errore finché non visualizzi il menu dei suggerimenti, quindi fai clic e seleziona Implementa membri.
  1. Seleziona tutti i metodi e premi Ok.

Passaggio 3: Implementa i metodi FakeTestRepository

Ora hai una classe FakeTestRepository con metodi "not implemented". Analogamente a come hai implementato FakeDataSource, FakeTestRepository sarà supportato da una struttura di dati, anziché gestire una mediazione complessa tra origini dati locali e remote.

Tieni presente che il tuo FakeTestRepository non deve utilizzare FakeDataSource o altro; deve solo restituire output falsi realistici in base agli input. Utilizzerai un LinkedHashMap per archiviare l'elenco delle attività e un MutableLiveData per le attività osservabili.

  1. In FakeTestRepository, aggiungi sia una variabile LinkedHashMap che rappresenta l'elenco corrente delle attività sia un MutableLiveData per le attività osservabili.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

Implementa i seguenti metodi:

  1. getTasks: questo metodo deve prendere tasksServiceData, trasformarlo in un elenco utilizzando tasksServiceData.values.toList() e restituirlo come risultato Success.
  2. refreshTasks: aggiorna il valore di observableTasks in modo che corrisponda a quello restituito da getTasks().
  3. observeTasks: crea una coroutine utilizzando runBlocking ed esegui refreshTasks, quindi restituisce observableTasks.

Di seguito è riportato il codice per questi metodi.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

Passaggio 4: Aggiungere un metodo per il test a addTasks

Durante i test, è meglio avere già alcuni Tasks nel repository. Potresti chiamare saveTask più volte, ma per semplificare questa operazione, aggiungi un metodo helper specifico per i test che ti consente di aggiungere attività.

  1. Aggiungi il metodo addTasks, che accetta un vararg di attività, aggiunge ciascuna al HashMap e poi aggiorna le attività.

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

A questo punto hai un repository fittizio per i test con alcuni dei metodi chiave implementati. Poi, utilizzalo nei test.

In questa attività utilizzerai una classe fittizia all'interno di un ViewModel. Utilizza l'inserimento delle dipendenze del costruttore per inserire le due origini dati tramite l'inserimento delle dipendenze del costruttore aggiungendo una variabile TasksRepository al costruttore di TasksViewModel.

Questa procedura è leggermente diversa con i modelli di visualizzazione perché non vengono creati direttamente. Ad esempio:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


Come nel codice precedente, utilizzi il viewModel's delegato della proprietà che crea il modello di visualizzazione. Per modificare la modalità di creazione del modello di visualizzazione, devi aggiungere e utilizzare un ViewModelProvider.Factory. Se non hai familiarità con ViewModelProvider.Factory, puoi scoprire di più qui.

Passaggio 1: Creare e utilizzare un ViewModelFactory in TasksViewModel

Inizia aggiornando le classi e il test relativi alla schermata Tasks.

  1. Apri TasksViewModel.
  2. Modifica il costruttore di TasksViewModel in modo che accetti TasksRepository anziché costruirlo all'interno della classe.

TasksViewModel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

Poiché hai modificato il costruttore, ora devi utilizzare una factory per costruire TasksViewModel. Inserisci la classe factory nello stesso file di TasksViewModel, ma puoi anche inserirla in un file separato.

  1. Nella parte inferiore del file TasksViewModel, al di fuori della classe, aggiungi un TasksViewModelFactory che accetta un TasksRepository semplice.

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


Questo è il modo standard per modificare la struttura dei ViewModel. Ora che hai la factory, usala ovunque costruisci il tuo modello di visualizzazione.

  1. Aggiorna TasksFragment per utilizzare la fabbrica.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Esegui il codice della tua app e assicurati che tutto funzioni ancora.

Passaggio 2: Utilizza FakeTestRepository all'interno di TasksViewModelTest

Ora, invece di utilizzare il repository reale nei test del view model, puoi utilizzare il repository fittizio.

  1. Apri TasksViewModelTest.
  2. Aggiungi una proprietà FakeTestRepository in TasksViewModelTest.

TaskViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. Aggiorna il metodo setupViewModel per creare un FakeTestRepository con tre attività, quindi crea tasksViewModel con questo repository.

TasksViewModelTest.kt

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. Poiché non utilizzi più il codice AndroidX Test ApplicationProvider.getApplicationContext, puoi anche rimuovere l'annotazione @RunWith(AndroidJUnit4::class).
  2. Esegui i test e assicurati che funzionino ancora.

Utilizzando l'inserimento delle dipendenze del costruttore, ora hai rimosso DefaultTasksRepository come dipendenza e l'hai sostituito con FakeTestRepository nei test.

Passaggio 3: Aggiorna anche il frammento TaskDetail e il ViewModel

Apporta le stesse modifiche per TaskDetailFragment e TaskDetailViewModel. In questo modo, il codice sarà pronto per quando scriverai i test TaskDetail.

  1. Apri TaskDetailViewModel.
  2. Aggiorna il costruttore:

TaskDetailViewModel.kt

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. In fondo al file TaskDetailViewModel, al di fuori della classe, aggiungi un TaskDetailViewModelFactory.

TaskDetailViewModel.kt

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. Aggiorna TasksFragment per utilizzare la fabbrica.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Esegui il codice e assicurati che tutto funzioni.

Ora puoi utilizzare un FakeTestRepository anziché il repository reale in TasksFragment e TasksDetailFragment.

Successivamente, scriverai test di integrazione per testare le interazioni tra il fragment e il view model. Scoprirai se il codice del modello di visualizzazione aggiorna correttamente la tua UI. Per farlo, utilizzi

  • il pattern ServiceLocator
  • le librerie Espresso e Mockito

I test di integrazione testano l'interazione di diverse classi per assicurarsi che si comportino come previsto quando vengono utilizzate insieme. Questi test possono essere eseguiti localmente (set di origine test) o come test di strumentazione (set di origine androidTest).

Nel tuo caso, prenderai ogni fragment e scriverai test di integrazione per il fragment e il modello di visualizzazione per testare le funzionalità principali del fragment.

Passaggio 1: Aggiungere dipendenze Gradle

  1. Aggiungi le seguenti dipendenze Gradle.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "junit:junit:$junitVersion"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

Queste dipendenze includono:

  • junit:junit: JUnit, necessario per scrivere istruzioni di test di base.
  • androidx.test:core: libreria di test AndroidX di base
  • kotlinx-coroutines-test: la libreria di test delle coroutine
  • androidx.fragment:fragment-testing: libreria di test AndroidX per creare frammenti nei test e modificarne lo stato.

Poiché utilizzerai queste librerie nel set di origini androidTest, utilizza androidTestImplementation per aggiungerle come dipendenze.

Passaggio 2: Crea una classe TaskDetailFragmentTest

TaskDetailFragment mostra informazioni su una singola attività.

Inizierai scrivendo un test del fragment per TaskDetailFragment, poiché ha funzionalità piuttosto di base rispetto agli altri fragment.

  1. Apri taskdetail.TaskDetailFragment.
  2. Genera un test per TaskDetailFragment, come hai fatto in precedenza. Accetta le scelte predefinite e inseriscile nel set di origine androidTest (NON nel set di origine test).

  1. Aggiungi le seguenti annotazioni alla classe TaskDetailFragmentTest.

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

Lo scopo di queste annotazioni è:

  • @MediumTest: contrassegna il test come test di integrazione "a esecuzione media" (rispetto ai test delle unità @SmallTest e ai test end-to-end di grandi dimensioni @LargeTest). In questo modo puoi raggruppare e scegliere le dimensioni del test da eseguire.
  • @RunWith(AndroidJUnit4::class): utilizzato in qualsiasi classe che utilizza AndroidX Test.

Passaggio 3: Avviare un fragment da un test

In questa attività, avvierai TaskDetailFragment utilizzando la libreria AndroidX Testing. FragmentScenario è una classe di AndroidX Test che racchiude un fragment e ti consente di controllare direttamente il ciclo di vita del fragment per i test. Per scrivere test per i fragment, crea un FragmentScenario per il fragment che stai testando (TaskDetailFragment).

  1. Copia questo test in TaskDetailFragmentTest.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

Questo codice:

Questo non è ancora un test completo, perché non afferma nulla. Per ora, esegui il test e osserva cosa succede.

  1. Si tratta di un test strumentato, quindi assicurati che l'emulatore o il dispositivo sia visibile.
  2. Esegui il test.

Devono succedere alcune cose.

  • Innanzitutto, poiché si tratta di un test strumentato, il test verrà eseguito sul dispositivo fisico (se connesso) o su un emulatore.
  • Dovrebbe avviare il frammento.
  • Nota come non naviga in altri frammenti e non ha menu associati all'attività: è solo il frammento.

Infine, osserva attentamente e noterai che il frammento indica "Nessun dato" perché non carica correttamente i dati dell'attività.

Il test deve caricare TaskDetailFragment (operazione che hai già eseguito) e verificare che i dati siano stati caricati correttamente. Perché non ci sono dati? Questo accade perché hai creato un'attività, ma non l'hai salvata nel repository.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

Hai questo FakeTestRepository, ma devi trovare un modo per sostituire il repository reale con quello fittizio per il tuo frammento. Lo farai nel passaggio successivo.

In questa attività, fornirai il repository fittizio al frammento utilizzando un ServiceLocator. In questo modo, potrai scrivere i test di integrazione dei fragment e dei modelli di visualizzazione.

Non puoi utilizzare l'inserimento delle dipendenze del costruttore qui, come facevi prima, quando dovevi fornire una dipendenza al modello di visualizzazione o al repository. L'inserimento delle dipendenze del costruttore richiede di costruire la classe. Frammenti e attività sono esempi di classi che non vengono create e a cui in genere non si ha accesso al costruttore.

Poiché non costruisci il fragment, non puoi utilizzare l'inserimento delle dipendenze del costruttore per scambiare il test doppio del repository (FakeTestRepository) con il fragment. Utilizza invece il pattern Service Locator. Il pattern Service Locator è un'alternativa all'inserimento delle dipendenze. Ciò comporta la creazione di una classe singleton denominata "Service Locator", il cui scopo è fornire dipendenze sia per il codice normale che per quello di test. Nel codice dell'app normale (il set di origini main), tutte queste dipendenze sono le normali dipendenze dell'app. Per i test, modifichi il localizzatore di servizi per fornire versioni di test double delle dipendenze.

Non utilizzo di Service Locator


Utilizzo di un localizzatore di servizi

Per l'app di questo codelab:

  1. Crea una classe Service Locator in grado di costruire e archiviare un repository. Per impostazione predefinita, viene creato un repository "normale".
  2. Esegui il refactoring del codice in modo che quando hai bisogno di un repository, utilizzi il Service Locator.
  3. Nella classe di test, chiama un metodo sul localizzatore di servizi che sostituisce il repository "normale" con il test double.

Passaggio 1: Crea ServiceLocator

Creiamo una lezione di ServiceLocator. Verrà inserito nel set di origini principale con il resto del codice dell'app perché viene utilizzato dal codice dell'applicazione principale.

Nota: ServiceLocator è un singleton, quindi utilizza la parola chiave objectKotlin per la classe.

  1. Crea il file ServiceLocator.kt nel livello superiore del set di origini principale.
  2. Definisci un object chiamato ServiceLocator.
  3. Crea le variabili di istanza database e repository e impostale entrambe su null.
  4. Annota il repository con @Volatile perché potrebbe essere utilizzato da più thread (@Volatile è spiegato in dettaglio qui).

Il tuo codice dovrebbe essere simile a quello mostrato di seguito.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

Al momento, l'unica cosa che deve fare il tuo ServiceLocator è sapere come restituire un TasksRepository. Restituirà un DefaultTasksRepository preesistente o creerà e restituirà un nuovo DefaultTasksRepository, se necessario.

Definisci le seguenti funzioni:

  1. provideTasksRepository: fornisce un repository già esistente o ne crea uno nuovo. Questo metodo deve essere synchronized su this per evitare, in situazioni con più thread in esecuzione, di creare accidentalmente due istanze del repository.
  2. createTasksRepository: codice per creare un nuovo repository. Chiamerà createTaskLocalDataSource e creerà un nuovo TasksRemoteDataSource.
  3. createTaskLocalDataSource: codice per creare una nuova origine dati locale. Verrà chiamato il numero createDataBase.
  4. createDataBase: codice per la creazione di un nuovo database.

Il codice completato è riportato di seguito.

ServiceLocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

Passaggio 2: Utilizzare ServiceLocator nell'applicazione

Apporterai una modifica al codice dell'applicazione principale (non ai test) in modo da creare il repository in un unico posto, ovvero ServiceLocator.

È importante creare una sola istanza della classe del repository. Per assicurarti che sia così, utilizzerai il localizzatore di servizi nella classe Application.

  1. Al livello superiore della gerarchia dei pacchetti, apri TodoApplication e crea un val per il repository e assegnagli un repository ottenuto utilizzando ServiceLocator.provideTaskRepository.

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

Ora che hai creato un repository nell'applicazione, puoi rimuovere il vecchio metodo getRepository in DefaultTasksRepository.

  1. Apri DefaultTasksRepository ed elimina l'oggetto complementare.

DefaultTasksRepository.kt

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

Ora, ovunque utilizzavi getRepository, usa taskRepository dell'applicazione. In questo modo, anziché creare direttamente il repository, riceverai il repository fornito da ServiceLocator.

  1. Apri TaskDetailFragement e trova la chiamata a getRepository nella parte superiore della classe.
  2. Sostituisci questa chiamata con una chiamata che recupera il repository da TodoApplication.

TaskDetailFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. Fai lo stesso per TasksFragment.

TasksFragment.kt

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. Per StatisticsViewModel e AddEditTaskViewModel, aggiorna il codice che acquisisce il repository in modo che utilizzi il repository di TodoApplication.

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Esegui l'applicazione (non il test).

Poiché hai eseguito solo il refactoring, l'app dovrebbe funzionare allo stesso modo senza problemi.

Passaggio 3: Crea FakeAndroidTestRepository

Hai già un FakeTestRepository nel set di origini di test. Per impostazione predefinita, non puoi condividere le classi di test tra i set di origini test e androidTest. Pertanto, devi creare una classe FakeTestRepository duplicata nel set di origini androidTest e chiamarla FakeAndroidTestRepository.

  1. Fai clic con il tasto destro del mouse sul set di origini androidTest e crea un pacchetto di dati. Fai di nuovo clic con il tasto destro del mouse e crea un pacchetto sorgente .
  2. Crea una nuova classe in questo pacchetto di origine chiamato FakeAndroidTestRepository.kt.
  3. Copia il seguente codice in quel corso.

FakeAndroidTestRepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

Passaggio 4: Preparare ServiceLocator per i test

Ok, è il momento di utilizzare ServiceLocator per sostituire i test double durante il test. Per farlo, devi aggiungere del codice al codice ServiceLocator.

  1. Apri ServiceLocator.kt.
  2. Contrassegna il setter per tasksRepository come @VisibleForTesting. Questa annotazione è un modo per esprimere che il motivo per cui il setter è pubblico è dovuto ai test.

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

Che tu esegua il test da solo o in un gruppo di test, i risultati devono essere esattamente gli stessi. Ciò significa che i test non devono avere comportamenti dipendenti l'uno dall'altro (il che significa evitare di condividere oggetti tra i test).

Poiché ServiceLocator è un singleton, può essere condiviso accidentalmente tra i test. Per evitare questo problema, crea un metodo che reimposti correttamente lo stato di ServiceLocator tra un test e l'altro.

  1. Aggiungi una variabile di istanza chiamata lock con il valore Any.

ServiceLocator.kt

private val lock = Any()
  1. Aggiungi un metodo specifico per i test chiamato resetRepository che cancella il database e imposta sia il repository che il database su null.

ServiceLocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

Passaggio 5: Utilizzare ServiceLocator

In questo passaggio, utilizzi ServiceLocator.

  1. Apri TaskDetailFragmentTest.
  2. Dichiara una variabile lateinit TasksRepository.
  3. Aggiungi un metodo di configurazione e smontaggio per configurare un FakeAndroidTestRepository prima di ogni test e pulirlo dopo ogni test.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. Racchiudi il corpo della funzione activeTaskDetails_DisplayedInUi() in runBlockingTest.
  2. Salva activeTask nel repository prima di avviare il frammento.
repository.saveTask(activeTask)

Il test finale ha l'aspetto del codice riportato di seguito.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. Aggiungi un'annotazione per l'intera classe con @ExperimentalCoroutinesApi.

Al termine, il codice avrà questo aspetto.

TaskDetailFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. Esegui il test activeTaskDetails_DisplayedInUi().

Come prima, dovresti vedere il frammento, ma questa volta, poiché hai configurato correttamente il repository, vengono visualizzate le informazioni sull'attività.


In questo passaggio, utilizzerai la libreria di test UI Espresso per completare il tuo primo test di integrazione. Hai strutturato il codice in modo da poter aggiungere test con asserzioni per la tua UI. Per farlo, utilizzerai la libreria di test Espresso.

Espresso ti aiuta a:

  • Interagisci con le visualizzazioni, ad esempio facendo clic sui pulsanti, scorrendo una barra o scorrendo verso il basso una schermata.
  • Affermare che determinate visualizzazioni sono sullo schermo o in un determinato stato (ad esempio, che contengono un testo specifico o che una casella di controllo è selezionata e così via).

Passaggio 1: Annota la dipendenza Gradle

Avrai già la dipendenza principale di Espresso, poiché è inclusa nei progetti Android per impostazione predefinita.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core: questa dipendenza principale di Espresso è inclusa per impostazione predefinita quando crei un nuovo progetto Android. Contiene il codice di test di base per la maggior parte delle visualizzazioni e delle azioni.

Passaggio 2: Disattivare le animazioni

I test Espresso vengono eseguiti su un dispositivo reale e sono quindi test di strumentazione per natura. Un problema che si presenta è quello delle animazioni: se un'animazione è in ritardo e provi a verificare se una visualizzazione è sullo schermo, ma è ancora in corso, Espresso può non riuscire a eseguire un test. Ciò può rendere i test Espresso irregolari.

Per i test dell'interfaccia utente Espresso, è consigliabile disattivare le animazioni (inoltre, il test verrà eseguito più rapidamente):

  1. Sul dispositivo di test, vai a Impostazioni > Opzioni sviluppatore.
  2. Disattiva queste tre impostazioni: Scala animazione finestra, Scala animazione transizione e Scala durata animatore.

Passaggio 3: Esame di un test Espresso

Prima di scrivere un test Espresso, dai un'occhiata ad alcuni codici Espresso.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

Questa istruzione trova la visualizzazione della casella di controllo con l'ID task_detail_complete_checkbox, fa clic e poi verifica che sia selezionata.

La maggior parte delle istruzioni Espresso è composta da quattro parti:

1. Metodo Static Espresso

onView

onView è un esempio di metodo Espresso statico che avvia un'istruzione Espresso. onView è una delle più comuni, ma esistono altre opzioni, ad esempio onData.

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId è un esempio di ViewMatcher che ottiene una visualizzazione in base al suo ID. Esistono altri view matcher che puoi consultare nella documentazione.

3. ViewAction

perform(click())

Il metodo perform che accetta un ViewAction. Un ViewAction è un'azione che può essere eseguita sulla visualizzazione, ad esempio qui, si tratta di fare clic sulla visualizzazione.

4. ViewAssertion

check(matches(isChecked()))

check che richiede un ViewAssertion. ViewAssertioncontrolla o afferma qualcosa sulla visualizzazione. L'ViewAssertion più comune che utilizzerai è l'asserzione matches. Per completare l'asserzione, utilizza un altro ViewMatcher, in questo caso isChecked.

Tieni presente che non sempre chiami sia perform sia check in un'istruzione Espresso. Puoi avere istruzioni che fanno solo un'asserzione utilizzando check o solo un ViewAction utilizzando perform.

  1. Apri TaskDetailFragmentTest.kt.
  2. Aggiorna il test activeTaskDetails_DisplayedInUi.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

Ecco le istruzioni di importazione, se necessarie:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. Tutto ciò che segue il commento // THEN utilizza Espresso. Esamina la struttura del test e l'utilizzo di withId e verifica di poter fare asserzioni sull'aspetto della pagina dei dettagli.
  2. Esegui il test e conferma che venga superato.

Passaggio 4: (Facoltativo) Scrivi il tuo test espresso

Ora scrivi un test.

  1. Crea un nuovo test chiamato completedTaskDetails_DisplayedInUi e copia questo codice di base.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. Esaminando il test precedente, completa questo test.
  2. Esegui e conferma che il test venga superato.

Il completedTaskDetails_DisplayedInUi completato dovrebbe essere simile a questo codice.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

In questo ultimo passaggio imparerai a testare il componente di navigazione utilizzando un diverso tipo di test double chiamato mock e la libreria di test Mockito.

In questo codelab hai utilizzato un test double chiamato fake. I fake sono uno dei tanti tipi di test double. Quale test double devi utilizzare per testare il componente Navigation?

Pensa a come avviene la navigazione. Immagina di premere una delle attività in TasksFragment per andare alla schermata dei dettagli di un'attività.

Ecco il codice in TasksFragment che consente di passare a una schermata dei dettagli dell'attività quando viene premuto.

TasksFragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


La navigazione avviene a causa di una chiamata al metodo navigate. Se devi scrivere un'istruzione assert, non esiste un modo semplice per verificare se hai eseguito la navigazione fino a TaskDetailFragment. La navigazione è un'azione complessa che non produce un output o una modifica dello stato chiari, oltre all'inizializzazione di TaskDetailFragment.

Puoi asserire che il metodo navigate è stato chiamato con il parametro action corretto. Questo è esattamente ciò che fa un mock: controlla se sono stati chiamati metodi specifici.

Mockito è un framework per la creazione di test doppi. Sebbene la parola mock sia utilizzata nell'API e nel nome, non serve solo per creare mock. Può anche creare stub e spie.

Utilizzerai Mockito per creare un mock NavigationController che può verificare che il metodo navigate sia stato chiamato correttamente.

Passaggio 1: Aggiungere dipendenze Gradle

  1. Aggiungi le dipendenze di Gradle.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core: questa è la dipendenza Mockito.
  • dexmaker-mockito: questa libreria è necessaria per utilizzare Mockito in un progetto Android. Mockito deve generare classi in fase di runtime. Su Android, questa operazione viene eseguita utilizzando il codice byte dex, pertanto questa libreria consente a Mockito di generare oggetti durante l'esecuzione su Android.
  • androidx.test.espresso:espresso-contrib: questa libreria è composta da contributi esterni (da cui il nome) che contengono codice di test per visualizzazioni più avanzate, come DatePicker e RecyclerView. Contiene anche controlli di accessibilità e una classe chiamata CountingIdlingResource che verrà trattata in un secondo momento.

Passaggio 2: Crea TasksFragmentTest

  1. Apri TasksFragment.
  2. Fai clic con il tasto destro del mouse sul nome della classe TasksFragment e seleziona Genera, quindi Testa. Crea un test nel set di origini androidTest.
  3. Copia questo codice in TasksFragmentTest.

TasksFragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

Questo codice è simile al codice TaskDetailFragmentTest che hai scritto. Monta e smonta un FakeAndroidTestRepository. Aggiungi un test di navigazione per verificare che quando fai clic su un'attività nell'elenco delle attività, venga visualizzato il TaskDetailFragment corretto.

  1. Aggiungi il test clickTask_navigateToDetailFragmentOne.

TasksFragmentTest.kt

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. Utilizza la funzione mock di Mockito per creare un mock.

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

Per creare un mock in Mockito, passa la classe che vuoi simulare.

Il passaggio successivo consiste nell'associare NavController al frammento. onFragment consente di chiamare metodi sul fragment stesso.

  1. Rendi il nuovo mock l'NavController del fragmento.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Aggiungi il codice per fare clic sull'elemento in RecyclerView che contiene il testo "TITLE1".
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions fa parte della libreria espresso-contrib e ti consente di eseguire azioni Espresso su un RecyclerView.

  1. Verifica che navigate sia stato chiamato con l'argomento corretto.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

Il metodo verify di Mockito è ciò che rende questo test un mock: puoi confermare che il mock navController ha chiamato un metodo specifico (navigate) con un parametro (actionTasksFragmentToTaskDetailFragment con l'ID "id1").

Il test completo ha il seguente aspetto:

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. Esegui il test.

In sintesi, per testare la navigazione puoi:

  1. Utilizza Mockito per creare un mock NavController.
  2. Allega il NavController simulato al fragmento.
  3. Verifica che navigate sia stato chiamato con l'azione e i parametri corretti.

Passaggio 3: (Facoltativo) Scrivi clickAddTaskButton_navigateToAddEditFragment

Per vedere se riesci a scrivere un test di navigazione, prova questa attività.

  1. Scrivi il test clickAddTaskButton_navigateToAddEditFragment che verifica che se fai clic sul pulsante di azione rapida +, si apre la schermata AddEditTaskFragment.

La risposta è riportata di seguito.

TasksFragmentTest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

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_2


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

Scarica zip

Questo codelab ha trattato come configurare l'inserimento manuale delle dipendenze, un service locator e come utilizzare fakes e simulazioni nelle tue app Android Kotlin. In particolare:

  • Ciò che vuoi testare e la tua strategia di test determinano i tipi di test che implementerai per la tua app. I test unitari sono mirati e veloci. I test di integrazione verificano l'interazione tra le parti del programma. I test end-to-end verificano le funzionalità, hanno la massima fedeltà, sono spesso strumentati e potrebbero richiedere più tempo per essere eseguiti.
  • L'architettura della tua app influisce sulla difficoltà del test.
  • TDD o Test Driven Development è una strategia in cui scrivi prima i test, poi crei la funzionalità per superarli.
  • Per isolare parti della tua app per i test, puoi utilizzare i test doppi. Un test double è una versione di una classe creata appositamente per i test. Ad esempio, simuli l'ottenimento di dati da un database o da internet.
  • Utilizza l'iniezione delle dipendenze per sostituire una classe reale con una classe di test, ad esempio un repository o un livello di rete.
  • Utilizza i test strumentati (androidTest) per avviare i componenti UI.
  • Quando non puoi utilizzare l'inserimento delle dipendenze del costruttore, ad esempio per avviare un fragment, spesso puoi utilizzare un service locator. Il pattern Service Locator è un'alternativa all'inserimento delle dipendenze. Ciò comporta la creazione di una classe singleton denominata "Service Locator", il cui scopo è fornire dipendenze sia per il codice normale che per quello di test.

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.