Introduzione ai doppi di test e all'iniezione di dipendenza

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

Introduzione

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

  • Test delle unità di repository
  • Frammenti e test di integrazione del modello di vista
  • Test di navigazione con frammenti

Informazioni importanti

Dovresti acquisire familiarità con:

Obiettivi didattici

  • Come pianificare una strategia di test
  • Come creare e utilizzare doppi test, vale a dire falsi e simulazioni
  • Come utilizzare l'inserimento manuale delle dipendenze su Android per i test di unità e integrazioni
  • Come applicare il pattern Service Locator
  • Come testare repository, frammenti, visualizzare modelli e il componente Navigazione

Utilizzerai i seguenti concetti di libreria e codice:

In questo lab proverai a:

  • Scrivere test delle unità per un repository utilizzando un test doppio di dipendenza e una dipendenza.
  • Scrivere test delle unità per un modello di visualizzazione utilizzando un test di doppia analisi e di dipendenza.
  • Scrivere test di integrazione per i frammenti e i relativi modelli di visualizzazione utilizzando il framework di test dell'interfaccia utente di Espresso.
  • Scrivere test di navigazione utilizzando Mockito ed Espresso.

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

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

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

Dedica un momento ad 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 compilata. Esplora l'app procedendo nel seguente modo:

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

Passaggio 2: esplora il codice dell'app di esempio

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

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

Ecco un riepilogo dei pacchetti disponibili:

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

.addedittask

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

.data

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

.statistics

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

.taskdetail

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

.tasks

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

.util

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

Livello dati (.data)

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

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

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

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

Navigazione

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

In questo codelab, imparerai a testare repository, visualizzare modelli e frammenti utilizzando doppi di test e inserimento di dipendenze. Prima di approfondire l'argomento, è importante capire il ragionamento alla base di questi test.

Questa sezione illustra alcune best practice di test in generale perché sono valide per Android.

La Piramide di prova

Quando pensi a una strategia di test, esistono tre aspetti correlati ai test:

  • Ambito: in che misura il codice è attivo? I test possono essere eseguiti in un singolo metodo, in tutta l'applicazione o in una fase intermedia.
  • Velocità: a che velocità viene eseguito il test? La velocità di test può variare da millisecondi a diversi minuti.
  • Fedeltà: quant'è reale il mondo? Ad esempio, se il codice di test deve eseguire una richiesta di rete, il codice del test effettua effettivamente questa richiesta o il risultato è falso? Se il test comunica effettivamente con la rete, significa che ha una maggiore fedeltà. In caso contrario, il test potrebbe richiedere più tempo per l'esecuzione, potrebbe causare errori se la rete non è disponibile o essere costoso da utilizzare.

I punti di forza sono intrinseci tra questi aspetti. Ad esempio, velocità e fedeltà sono un compromesso: più rapido è il test, in generale minore è la fedeltà, e viceversa. Un modo comune per suddividere i test automatici è in queste tre categorie:

  • Test delle unità: sono test altamente mirati che vengono eseguiti su un singolo corso, in genere un unico metodo in quel corso. Se il test di un'unità non va a buon fine, puoi sapere esattamente dove si trova il problema. Hanno scarsa fedeltà poiché nel mondo reale la tua app richiede molto più tempo rispetto all'esecuzione di un solo metodo o classe. Sono abbastanza veloci da essere eseguiti ogni volta che modifichi il codice. Nella maggior parte dei casi, verranno eseguiti test in locale (nell'insieme di origini test). Esempio: test di singoli metodi in modelli e repository di visualizzazione.
  • Test di integrazione: questi elementi consentono di testare l'interazione di più classi per assicurarsi che si comportino come previsto quando vengono utilizzati insieme. Un modo per strutturare i test di integrazione consiste nel far testare una singola funzionalità, ad esempio la possibilità di salvare un'attività. Nonostante testano un ambito di codice più ampio rispetto ai test delle unità, sono comunque ottimizzati per essere eseguiti velocemente, rispetto alla fedeltà completa. Possono essere eseguiti localmente o come test di strumentazione, a seconda della situazione. Esempio: testare tutte le funzionalità di una singola coppia di modelli di vista e frammento.
  • Test end-to-end (E2e): testa una combinazione di funzionalità che funzionano insieme. Testano ampie parti dell'app, simulano il vero utilizzo reale e di solito sono lenti. Hanno la massima fedeltà e ti comunicano che la tua applicazione funziona nel suo insieme. In generale, questi test saranno test strumentali (nel set di origini androidTest)
    Esempio: avvio dell'intera app e test di alcune funzionalità insieme.

La proporzione consigliata di questi test è spesso rappresentata da una piramide e la maggior parte dei test consiste in test delle unità.

Architettura e test

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

Un approccio migliore potrebbe essere quello di suddividere la logica dell'applicazione in più metodi e classi, in modo da testare separatamente ogni elemento. L'architettura è un modo per dividere e organizzare il codice, semplificando i test di unità e integrazioni. L'app TO-DO su cui eseguirai il test segue una particolare architettura:



In questa lezione imparerai come testare parti dell'architettura precedente, isolando correttamente:

  1. Innanzitutto eseguirai il test dell'unità repository.
  2. Quindi, utilizzerai un modello di test doppio nel modello vista, che è necessario per eseguire i test delle unità e l'integrazione del modello vista.
  3. Dopodiché imparerai a scrivere test di integrazione per frammenti e relativi modelli di visualizzazione.
  4. Infine, imparerai a scrivere test di integrazione che includono il componente Navigazione.

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

Quando scrivi un test delle unità per una parte di un corso (un metodo o una piccola raccolta di metodi), l'obiettivo è testare il codice solo in quel corso.

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

Il tuo obiettivo è testare solo il codice della classe. Tuttavia, DefaultTaskRepository dipende da altre classi, come LocalTaskDataSource e RemoteTaskDataSource, per funzionare. Un altro modo per dire che è che LocalTaskDataSource e RemoteTaskDataSource sono dipendenze di DefaultTaskRepository.

Pertanto, ogni metodo in DefaultTaskRepository chiama i metodi per le classi dell'origine dati, che a loro volta eseguono altri metodi di chiamata per salvare informazioni nel 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 potresti effettuare al tuo repository. Questo metodo include la lettura da un database SQLite e l'esecuzione di chiamate di rete (la chiamata a updateTasksFromRemoteDataSource). Questo comporta molto più codice rispetto a solo del codice del repository.

Di seguito sono riportati alcuni motivi più specifici per cui l'esecuzione di test del repository è difficile:

  • È necessario pensare a come creare e gestire un database per eseguire anche i test più semplici per questo repository. Verranno visualizzate domande come "dovrebbe essere un test locale o strumentale?" e se dovresti utilizzare AndroidX Test per ottenere un ambiente Android simulato.
  • L'esecuzione di alcune parti del codice, come il codice di networking, può richiedere molto tempo, o talvolta anche non riuscire, creando test irregolari a lunga esecuzione.
  • I test potrebbero non essere in grado di diagnosticare il codice a causa di un errore del test. I test potrebbero iniziare a testare il codice non repository, quindi, ad esempio, il presunto test delle unità "repository" potrebbe non riuscire a causa di un problema in alcuni codici dipendenti, come il codice del database.

Doppio di prova

Per risolvere il problema, quando esegui il test del repository, non utilizzare il vero codice di rete o del database, ma usa un test doppio. Un doppio test è una versione di un corso creato appositamente per i test. Il suo scopo è sostituire la versione reale di una classe nei test. È simile a una stuntman che è un attore specializzato in acrobazie e sostituisce il vero attore con azioni pericolose.

Ecco alcuni tipi di doppi test:

Falso

Un test doppio con un'implementazione della classe "lavorativa", ma implementata in modo da renderla adatta ai test, ma non adatta alla produzione.

Simulazione

Un doppio test che monitora il metodo da chiamare. Quindi supera o non supera il test, a seconda che i metodi siano stati chiamati correttamente.

Stub

Un doppio di test che non include alcuna logica e restituisce solo il programma che restituisci. Ad esempio, è possibile programmare un StubTaskRepository per restituire determinate combinazioni di attività da getTasks.

Fumetto

Un test doppio che viene trasmesso ma non utilizzato, ad esempio se devi solo specificare come parametro. Se avessi un elemento NoOpTaskRepository, implementeresti semplicemente il codice TaskRepository con il codice no in uno qualsiasi dei metodi.

Spia

Un doppio test 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 ulteriori informazioni sui doppi del test, consulta la pagina Test sul WC: scopri il tuo test di doppio.

I doppi test più comuni utilizzati in Android sono Fake e Mock.

In questa attività creerai un doppio di test FakeDataSource per eseguire il test dell'unità DefaultTasksRepository disaccoppiato dalle origini dati effettive.

Passaggio 1: crea la classe FakeDataSource

In questo passaggio stai creando una classe chiamata FakeDataSouce, che sarà un doppio test di LocalDataSource e RemoteDataSource.

  1. Nel set di origine di test, fai clic con il pulsante destro del mouse e seleziona Nuovo pacchetto ->.

  1. Crea un pacchetto di dati con un pacchetto di origine 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 doppio test, deve essere in grado di sostituire le altre origini dati. Le origini dati indicate sono TasksLocalDataSource e TasksRemoteDataSource.

  1. Nota come entrambi implementano l'interfaccia di 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 spiega che non hai implementato i metodi richiesti per TasksDataSource.

  1. Utilizza il menu a risoluzione 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 doppio chiamato fasullo. Un falso è un doppio di test che ha un'implementazione "di lavoro" della classe, ma è stato implementato in un modo che lo rende utile per i test, ma non per la produzione. "Implementazione" significa che la classe produce output realistici in base agli input.

Ad esempio, l'origine dati falsi non si connetterà alla rete né salverà dati in un database, ma utilizzerà solo un elenco in memoria. Questo funzionerà come previsto. Con questi metodi il recupero o il salvataggio delle attività restituirà i risultati previsti, ma non potresti mai utilizzare questa implementazione in produzione, perché non viene salvata sul server o in un database.

Un FakeDataSource

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


Questo è l'elenco delle attività che"fake", che sono un database o una risposta del server. Per ora, l'obiettivo è testare il metodo repository's getTasks. Questa operazione chiama i metodi di origine dati getTasks, deleteAllTasks e saveTask.

Scrivi una versione falsa di questi metodi:

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

Questi metodi, implementati per FakeDataSource, hanno il seguente aspetto.

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 necessario:

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

Il funzionamento è simile a quello delle origini dati locali e remote.

In questo passaggio, utilizzerai una tecnica chiamata iniezione manuale di dipendenza in modo da poter utilizzare il doppio test di prova che hai appena creato.

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

Al momento, le dipendenze sono basate sul 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
}

Gli annunci taskLocalDataSource e tasksRemoteDataSource che crei e assegni all'interno di DefaultTasksRepository sono essenzialmente hardcoded. Non c'è modo di eseguire un doppio test.

Tuttavia, fornisci queste origini dati in classe, invece di eseguirne l'hard coding. L'indicazione delle dipendenze è nota come iniezione di dipendenza. Esistono diversi modi per fornire le dipendenze e quindi diversi tipi di inserimento delle dipendenze.

Iniezione di dipendenza del costruttore ti consente di scambiare il doppio del test passandolo al costruttore.

Nessuna iniezione

Iniezione

Passaggio 1: usa l'iniezione di dipendenza del costruttore in DefaultTasksRepository

  1. Cambia la scelta del costruttore di DefaultTaskRepository per assumere un Application e poi per le origini dati e il supervisore di coroutine (che dovrai sostituire anche per i test) come descritto più in dettaglio nella terza sezione della lezione sulle coroutine.

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 devi più creare le dipendenze.
  2. Elimina anche le vecchie variabili di istanza. Stai definendole 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 in modo che utilizzi 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'iniezione di dipendenza del costruttore!

Passaggio 2: utilizza FakeDataSource nei test

Ora che il tuo codice utilizza un'iniezione di dipendenza da costruttori, puoi utilizzare l'origine dati falsa per testare il tuo DefaultTasksRepository.

  1. Fai clic con il pulsante destro del mouse sul nome della classe DefaultTasksRepository e seleziona Genera, quindi Test.
  2. Segui le istruzioni per creare DefaultTasksRepositoryTest nel set di origini test.
  3. Nella parte superiore del nuovo corso DefaultTasksRepositoryTest, aggiungi le variabili membri riportate di seguito per rappresentare i dati nelle tue origini dati false.

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 FakeDataSource per i membri (una per ciascuna origine dati per il repository) e una variabile per DefaultTasksRepository da sottoporre a test.

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 elemento DefaultTasksRepository testabile. Questa DefaultTasksRepository utilizzerà il tuo test doppio, FakeDataSource.

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

Il metodo finale dovrebbe essere simile al seguente codice.

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: scrittura del test getTasks() di DefaultTasksRepository

È ora di scrivere un test DefaultTasksRepository.

  1. Scrivi un test per il metodo getTasks del repository. Quando chiami getTasks con true, il che significa che deve essere ricaricato dall'origine dati remota, verifica che 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))
    }

Durante la chiamata riceverai un errore getTasks:

Passaggio 4: aggiungi runBlockTest

L'errore correlato alla coroutine è previsto perché getTasks è una funzione suspend ed è necessario lanciare una coroutine per chiamarla. Per farlo, ti serve un ambito coroutine. Per risolvere questo errore, dovrai aggiungere alcune dipendenze graduali per la gestione delle coroutine nei test.

  1. Aggiungi le dipendenze obbligatorie per il test delle coroutine all'origine di test impostata utilizzando testImplementation.

app/build.gradle

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

Non dimenticare di sincronizzare.

kotlinx-coroutines-test è la libreria di test per le coroutine, progettata appositamente per testare le coroutine. Per eseguire i test, utilizza la funzione runBlockingTest. Questa è una funzione fornita dalla libreria di test delle coroutine. Prende un blocco di codice e poi esegue questo blocco di codice in un particolare contesto coroutine che viene eseguito in modo sincrono e immediato, vale a dire che le azioni avvengono in ordine deterministico. Essenzialmente, le coroutine funzionano come non coroutine, perciò sono ideate per testare il codice.

Utilizza runBlockingTest nelle tue classi di prova 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 il corso. Questo significa che sai che stai utilizzando un API coroutine sperimentale (runBlockingTest) nella classe. Senza questo link, riceverai un avviso.
  2. Torna alla DefaultTasksRepositoryTest, aggiungi runBlockingTest in modo che l'intero test si comporti come un blocco di codice

Questo test finale ha l'aspetto del seguente codice.

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 di getTasks_requestsAllTasksFromRemoteDataSource e verifica che funzioni e l'errore non sia più visibile.

Hai appena visto come unitare i test di un repository. Nei passaggi successivi, utilizzerai di nuovo l'inserimento delle dipendenze e creerai un altro test doppio, questa volta per mostrare come scrivere i test delle unità e delle integrazioni per i modelli di visualizzazione.

I test delle unità dovrebbero testare solo la classe o il metodo che ti interessa. Questa operazione è nota come test in isolamento, in cui isola chiaramente la tua "unità" e test solo il codice che fa parte di tale unità.

Pertanto, TasksViewModelTest dovrebbe testare solo il codice TasksViewModel e non le classi di database, di rete o del repository. Di conseguenza, per i tuoi modelli di vista, come hai fatto per il repository, creerai un repository falso e applicherai l'inserimento di dipendenze per utilizzarlo nei test.

In questa attività, applicherai l'inserimento di dipendenze per visualizzare i modelli.

Passaggio 1. Crea un'interfaccia repository di repository

Il primo passaggio per l'utilizzo dell'inserimento di dipendenze da parte dei costruttori consiste nel creare un'interfaccia comune condivisa tra il falso e la classe reale.

Che aspetto ha questa pratica? Tieni presente che TasksRemoteDataSource, TasksLocalDataSource e FakeDataSource hanno tutti la stessa interfaccia: TasksDataSource. Ciò ti consente di specificare nel costruttore di DefaultTasksRepository che assumi un TasksDataSource.

DefaultTasksRepository.kt

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

Questo ci consente di scambiare il tuo FakeDataSource!

Quindi, 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 pulsante destro del mouse sul nome del corso. quindi seleziona Refactoring -> Estrai -> interfaccia.

  1. Scegli Estrai in un file separato.

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


  1. Fai clic su Refactoring. La nuova interfaccia TasksRepository dovrebbe apparire nel pacchetto data/source.

E ora DefaultTasksRepository implementa TasksRepository.

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

Passaggio 2. Crea FakeTestRepository

Ora che disponi dell'interfaccia, puoi creare il doppio del test DefaultTaskRepository.

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

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Ti verrà chiesto di implementare i metodi dell'interfaccia.

  1. Passa il mouse sopra l'errore fino a visualizzare il menu dei suggerimenti, quindi fai clic e seleziona Implementa membri.
  1. Seleziona tutti i metodi e premi OK.

Passaggio 3. Implementare metodi FakeTestRepository

Ora hai una classe FakeTestRepository con metodi "non implementati". Come per l'implementazione di FakeDataSource, il FakeTestRepository verrà supportato da una struttura di dati, anziché gestire una mediazione complicata tra origini dati locali e remote.

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

  1. In FakeTestRepository, aggiungi sia una variabile LinkedHashMap che rappresenti l'elenco corrente di attività sia una 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 dovrebbe prendere il tasksServiceData e trasformarlo in un elenco utilizzando tasksServiceData.values.toList() e poi restituire il risultato come Success.
  2. refreshTasks: aggiorna il valore observableTasks in modo che sia quello restituito da getTasks().
  3. observeTasks: crea una coroutine utilizzando runBlocking ed esegui refreshTasks, quindi restituisce observableTasks.

Di seguito è riportato il codice per tali 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. Aggiungi un metodo per il test ad addTasks

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

  1. Aggiungi il metodo addTasks, che prevede un vararg di attività, li aggiunge a HashMap e 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 falso per i test, in cui sono implementati alcuni dei metodi principali. Dopodiché, utilizzalo nei tuoi test.

In questa attività utilizzerai un corso falso all'interno di un ViewModel. Utilizza l'iniezione di dipendenza del costruttore, per acquisire le due origini dati tramite l'inserimento di dipendenze del costruttore aggiungendo una variabile TasksRepository al costruttore di TasksViewModel.

La procedura è leggermente diversa per i modelli di visualizzazione, perché non li costruisci direttamente. Ad esempio:

class TasksFragment : Fragment() {

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

}


Come nel codice precedente, stai utilizzando la delega della proprietà viewModel's che crea il modello di visualizzazione. Per modificare la struttura del modello di visualizzazione, devi aggiungere e utilizzare un elemento ViewModelProvider.Factory. Se non conosci ViewModelProvider.Factory, puoi trovare ulteriori informazioni qui.

Passaggio 1. Creare e utilizzare un ViewModelFactory in TasksViewModel

Inizierai ad aggiornare i corsi e i test relativi alla schermata Tasks.

  1. Apri TasksViewModel.
  2. Cambia il costruttore di TasksViewModel in modo che accetti TasksRepository invece di 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 
}

Dal momento che hai cambiato il costruttore, ora devi utilizzare una fabbrica per costruire TasksViewModel. Inserisci la classe di fabbrica nello stesso file della TasksViewModel, ma puoi anche inserirla in un file a sé stante.

  1. Nella parte inferiore del file TasksViewModel, fuori dal corso, aggiungi un TasksViewModelFactory che contenga un semplice TasksRepository.

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 delle ViewModel. Ora che disponi di fabbrica, puoi utilizzarlo ovunque costruisci il tuo modello di vista.

  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. Usa FakeTestRepository all'interno di TasksViewModelTest

Anziché utilizzare il repository reale nei test del modello di visualizzazione, puoi utilizzare il repository falso.

  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 costruisci il 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, quindi verifica che funzionino ancora.

Utilizzando l'inserimento di dipendenze costruttore, hai rimosso DefaultTasksRepository come dipendenza e l'hai sostituito con il FakeTestRepository nei test.

Passaggio 3. Aggiorna anche Frammento TaskDettagli e Visualizzazione modello

Apporta esattamente le stesse modifiche per TaskDetailFragment e TaskDetailViewModel. In questo modo, il codice verrà preparato per la scrittura dei prossimi TaskDetail test.

  1. Apri TaskDetailViewModel.
  2. Aggiorna il costruttore:

TaskDetailsViewmodel.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, all'esterno del corso, aggiungi un TaskDetailViewModelFactory.

TaskDetailsViewmodel.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.

Dopodiché scriverai i test di integrazione per testare le interazioni con i frammenti e le visualizzazioni del modello. Scoprirai se il codice del modello di visualizzazione aggiorna correttamente l'interfaccia utente. A questo scopo, utilizzi

  • il pattern ServiceLocator
  • le biblioteche Espresso e Mockito

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

Nel tuo caso, dovrai svolgere ogni test di integrazione e scrittura del frammento e del modello di visualizzazione per testare le principali funzionalità del frammento.

Passaggio 1. Aggiungi dipendenze Gradle

  1. Aggiungi le seguenti dipendenze di 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"

tra cui:

  • junit:junit: JUnit, necessario per scrivere istruzioni di prova di base.
  • androidx.test:core-Libreria test AndroidX principale
  • kotlinx-coroutines-test: la libreria per i test delle coroutine
  • androidx.fragment:fragment-testing: libreria di test di AndroidX per la creazione di frammenti nei test e la modifica dello stato.

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

Passaggio 2. Crea una classe TaskDetailsFragmentTest

Il campo TaskDetailFragment mostra le informazioni relative a una singola attività.

Inizierai scrivendo un test dei frammenti per l'elemento TaskDetailFragment, in quanto ha una funzionalità di base abbastanza elevata rispetto agli altri frammenti.

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

  1. Aggiungi le seguenti annotazioni alla classe TaskDetailFragmentTest.

TaskDettagliFragmentTest.kt

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

}

Lo scopo di questa annotazione è:

  • @MediumTest: contrassegna il test come un test di integrazione "tempo di esecuzione medio" (rispetto ai test delle unità di @SmallTest e ai test end-to-end di @LargeTest). In questo modo puoi raggrupparlo e scegliere le dimensioni del test da eseguire.
  • @RunWith(AndroidJUnit4::class): utilizzato in qualsiasi corso con AndroidX Test.

Passaggio 3. Avviare un frammento da un test

In questa attività, avvierai TaskDetailFragment utilizzando la libreria di test di AndroidX. FragmentScenario è una classe di AndroidX Test che esegue il wrapping in funzione di un frammento e fornisce un controllo diretto sul ciclo di vita del frammento per il test. Per scrivere test per i frammenti, devi creare una FragmentScenario per il frammento che stai testando (TaskDetailFragment).

  1. Copia questo test in TaskDetailFragmentTest.

TaskDettagliFragmentTest.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 è riportato sopra:

  • Crea un'attività.
  • Crea un elemento Bundle, che rappresenta gli argomenti dei frammenti per l'attività che viene trasferita nel frammento.
  • La funzione launchFragmentInContainer crea un elemento FragmentScenario con questo bundle e un tema.

Non è ancora un test terminato, perché non sta rivendicando nulla. Per ora, esegui il test e osserva cosa succede.

  1. Questo è un test strumentale, quindi assicurati che l'emulatore o il tuo dispositivo sia visibile.
  2. Esegui il test.

Dovrebbe succedere qualcosa.

  • Innanzitutto, poiché si tratta di un test con strumentazione, il test viene eseguito sul dispositivo fisico (se collegato) o su un emulatore.
  • Dovrebbe avviare il frammento.
  • Nota come non naviga in nessun altro frammento o non ha menu associati all'attività: si tratta solo del frammento.

Infine, controlla attentamente e nota che il frammento indica che non ci sono dati in quanto non riesce a caricare correttamente i dati dell'attività.

Il test deve caricare la TaskDetailFragment (azione completata) e affermare che i dati sono stati caricati correttamente. Perché non esistono dati? 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 hai bisogno di un modo per sostituire il tuo repository reale con quello falso per il tuo frammento. Lo farai ora!

In questa attività dovrai fornire il tuo repository falso al tuo frammento utilizzando un ServiceLocator. In questo modo, potrai scrivere il frammento e visualizzare i test di integrazione del modello.

Qui non puoi utilizzare l'inserimento di dipendenze dei costruttori, come hai fatto in precedenza, quando è stato necessario fornire una dipendenza al modello di visualizzazione o al repository. L'inserimento di dipendenze del costruttore richiede la creazione della classe. Frammenti e attività sono esempi di classi di cui non sei tu a costruire e in genere non hai accesso al costruttore.

Poiché non costruisci il frammento, non puoi utilizzare l'iniezione di dipendenza del costruttore per scambiare il doppio del test del repository (FakeTestRepository) con il frammento. Utilizza invece il pattern Service Locator. Il pattern Service Locator è un'alternativa all'iniezione di dipendenza. Implica la creazione di una classe singleton denominata "Locator di servizio", il cui scopo è fornire dipendenze, sia per il codice normale sia per il codice di test. Nel codice dell'app normale (il set di origini main), tutte queste dipendenze sono dipendenze delle app normali. Per i test, devi modificare Service Locator per fornire versioni doppie delle dipendenze del test.

Service Locator non utilizzato


Utilizzare un Service Locator

Per questa app di codelab, segui questi passaggi:

  1. Creare una classe Service Locator in grado di creare e archiviare un repository. Per impostazione predefinita, crea un repository "normal".
  2. Esegui il refactoring del codice in modo che, quando è necessario un repository, utilizza Service Locator.
  3. Nella tua classe di test, chiama un metodo nel Service Locator che sostituisca il repository "normal" con il doppio del test.

Passaggio 1. Creazione di ServiceLocator

Ora creiamo un corso ServiceLocator. Risiederà nell'origine principale impostata con il resto del codice dell'app perché è utilizzata dal codice dell'applicazione principale.

Nota: ServiceLocator è un singolo, quindi utilizza la parola chiave Kotlin object per il corso.

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

Il 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 ServiceLocator deve fare è sapere come restituire un TasksRepository. Verrà restituito un DefaultTasksRepository preesistente o verrà restituito un nuovo DefaultTasksRepository, se necessario.

Definisci le seguenti funzioni:

  1. provideTasksRepository: fornisce un repository già esistente o ne crea uno nuovo. Questo metodo dovrebbe essere synchronized su this per evitare, in situazioni con più thread in esecuzione, di creare accidentalmente due istanze di repository.
  2. createTasksRepository: codice per la creazione di un nuovo repository. Chiamerà createTaskLocalDataSource e creerà un nuovo TasksRemoteDataSource.
  3. createTaskLocalDataSource: codice per la creazione di una nuova origine dati locale. Chiamerà 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. Utilizzo di ServiceLocator nell'applicazione

Stai modificando il codice dell'applicazione principale (non i test) per poter creare il repository in un unico posto, ServiceLocator.

È importante creare una sola istanza della classe del repository. Per farlo, utilizzerai il localizzatore di servizi nella mia classe Application.

  1. Al livello più alto della gerarchia di pacchetti, apri TodoApplication e crea un val per il repository, quindi 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 companion.

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 usavi getRepository, usa invece taskRepository dell'applicazione. In questo modo, anziché creare direttamente il repository, riceverai lo stesso repository fornito da ServiceLocator.

  1. Apri TaskDetailFragement e trova la chiamata al numero getRepository nella parte superiore del corso.
  2. Sostituisci questa chiamata con una chiamata che riceve il repository da TodoApplication.

TaskDettagliFragment.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. Ripeti l'operazione 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 per utilizzarlo da TodoApplication.

TasksFragment.kt

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



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Esegui la tua applicazione (non il test).

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

Passaggio 3. Creazione FakeAndroidTestRepository

È già presente un valore FakeTestRepository nell'insieme di origini di test. Per impostazione predefinita, non puoi condividere classi di test tra i set di origini test e androidTest. Devi quindi creare una classe FakeTestRepository duplicata nel set di origini androidTest e chiamarla FakeAndroidTestRepository.

  1. Fai clic con il pulsante destro del mouse sul set di origini androidTest e crea un pacchetto di dati. Fai di nuovo clic con il pulsante destro del mouse e crea un pacchetto sorgente.
  2. Crea un nuovo corso in questo pacchetto di origine chiamato FakeAndroidTestRepository.kt.
  3. Copia il codice seguente 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 il ServiceLocator per i test

Ok, è il momento di usare ServiceLocator per eseguire il doppio del test durante il test. A tale scopo, devi aggiungere del codice al tuo 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

Sia che esegui il test da solo o in un gruppo, i test dovrebbero essere gli stessi. Ciò significa che i test non dovrebbero avere comportamenti dipendenti l'uno dall'altro (evitando la condivisione di oggetti tra i test).

Poiché ServiceLocator è un singolo, è possibile che venga condiviso accidentalmente tra i test. Per evitare che ciò accada, crea un metodo che reimposti correttamente lo stato di ServiceLocator tra un test e l'altro.

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

ServiceLocator.kt

private val lock = Any()
  1. Aggiungi un metodo specifico di test denominato resetRepository, che cancelli il database e imposti sia il repository sia 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. Usare ServiceLocator

In questo passaggio, utilizzerai ServiceLocator.

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

TaskDettagliFragmentTest.kt

    private lateinit var repository: TasksRepository

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

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

Il test finale sarà simile al seguente codice.

TaskDettagliFragmentTest.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. Annota l'intero corso con @ExperimentalCoroutinesApi.

Al termine, il codice sarà simile a questo.

TaskDettagliFragmentTest.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 in precedenza, dovresti vedere il frammento, tranne che questa volta, perché hai configurato correttamente il repository, ora mostra le informazioni sull'attività.


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

Espresso ti aiuta a:

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

Passaggio 1. Dipendenza Gradle nota

Avrai già la principale dipendenza Espresso perché è 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 su di essi.

Passaggio 2. Disattivare le animazioni

I test Espresso vengono eseguiti su un dispositivo reale e di conseguenza sono test di strumentazione per natura. Un problema che si verifica è rappresentato dalle animazioni: se un'animazione è in ritardo e provi a verificare se una visualizzazione è sullo schermo, ma l'animazione è ancora in corso, Espresso può perdere accidentalmente un test. I test Espresso potrebbero essere irregolari.

Per i test dell'interfaccia utente di Espresso, è buona norma disattivare le animazioni (anche il test verrà eseguito più velocemente!):

  1. Sul dispositivo di test, vai a Impostazioni.
  2. Disattiva le tre impostazioni seguenti: Scala animazione finestra, Scala animazione di transizione e Scala animazione animazione.

Passaggio 3. Dai un'occhiata a un test Espresso

Prima di scrivere un test Espresso, dai un'occhiata a questo codice.

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

Questa istruzione ha il compito di trovare la visualizzazione della casella di controllo con l'ID task_detail_complete_checkbox, fare clic su di essa e affermare che è selezionata.

La maggior parte delle dichiarazioni Espresso è costituita da quattro parti:

1. Metodo espresso espresso

onView

onView è un esempio di metodo Espresso statico che avvia un'istruzione Espresso. onView è uno dei più comuni, ma ci sono altre opzioni, come onData.

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId è un esempio di ViewMatcher che riceve una vista in base al suo ID. Puoi trovare altri matcher di visualizzazione nella documentazione.

3. ViewAction

perform(click())

Il metodo perform che richiede una ViewAction. ViewAction può essere eseguito dalla vista, ad esempio facendo clic sulla vista.

4. ViewAssertion (Visualizza asserzione)

check(matches(isChecked()))

check che richiede una ViewAssertion. ViewAssertion controllano o affermano qualcosa sulla vista. Il ViewAssertion che utilizzerai più spesso è l'asserzione di 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 dichiarazioni che fanno solo una dichiarazione utilizzando check o semplicemente una ViewAction utilizzando perform.

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

TaskDettagliFragmentTest.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 necessario:

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 di // THEN utilizza Espresso. Esamina la struttura dei test e l'uso di withId e controlla se l'aspetto della pagina dei dettagli deve essere corretto.
  2. Esegui il test e verifica che superi.

Passaggio 4. (Facoltativo) Scrivi il tuo Espresso Test

Ora scrivi un test in autonomia.

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

TaskDettagliFragmentTest.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, completalo.
  2. Esegui e conferma i test superati.

L'elemento completedTaskDetails_DisplayedInUi completato dovrebbe avere il seguente codice.

TaskDettagliFragmentTest.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 quest'ultimo passaggio, imparerai a testare il componente Navigazione, utilizzando un tipo di test doppio diverso detto fittizia e la libreria di test Mockito.

In questo codelab hai utilizzato un doppio di test chiamato "false". I falsi sono uno dei molti tipi di doppi test. Quale doppio test devi utilizzare per testare il componente Navigazione?

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

Ecco il codice di TasksFragment che apre 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 si verifica a causa di una chiamata al metodo navigate. Se hai bisogno di scrivere una dichiarazione, non esiste un modo semplice per verificare se hai raggiunto TaskDetailFragment. La navigazione è un'azione complicata che non comporta una chiara variazione dell'output o dello stato, oltre all'inizializzazione di TaskDetailFragment.

Puoi affermare che il metodo navigate è stato chiamato con il parametro di azione corretto. È esattamente la funzione di un test fittizio: controlla se sono stati chiamati metodi specifici.

Mockito è un framework per eseguire i doppi del test. La parola fittizia viene utilizzata nell'API e nel nome, ma non viene utilizzata solo per le simulazioni. Può anche produrre stub e spie.

Utilizzerai Mockito per creare una simulazione di NavigationController in grado di affermare che il metodo di navigazione è stato chiamato correttamente.

Passaggio 1. Aggiungi dipendenze Gradle

  1. Aggiungi le dipendenze del 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 dipendenza di Mockito.
  • dexmaker-mockito: questa libreria è obbligatoria per usare Mockito in un progetto Android. Mockito deve generare le classi al momento dell'esecuzione. Su Android, questa operazione viene eseguita utilizzando il codice byte dex, quindi questa libreria consente a Mockito di generare oggetti durante il runtime su Android.
  • androidx.test.espresso:espresso-contrib: questa libreria è composta da contributi esterni (da cui deriva il nome) che contengono il codice di test per visualizzazioni più avanzate, come DatePicker e RecyclerView. Contiene inoltre controlli di accessibilità e una classe denominata CountingIdlingResource, trattata in un secondo momento.

Passaggio 2. Crea TasksFragmentTest

  1. Apri TasksFragment.
  2. Fai clic con il pulsante destro del mouse sul nome della classe TasksFragment e seleziona Genera, quindi Test. Crea un test nel set di origini androidTest.
  3. Copia questo codice nel 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. Si configura e elimina un FakeAndroidTestRepository. Aggiungi un test di navigazione per provare che, quando fai clic su un'attività nell'elenco, viene aperto il TaskDetailFragment corretto.

  1. Aggiungi la prova 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. Usa la funzione mock di Mockito per creare un gioco di simulazione.

TasksFragmentTest.kt

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

Per simulare in Mockito, trasmetti il corso che vuoi simulare.

Il prossimo passo è associare il tuo NavController al frammento. onFragment ti consente di chiamare metodi sul frammento stesso.

  1. Rendi il tuo nuovo modello fittizio il NavController del frammento.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Aggiungi il codice per fare clic sull'elemento nell'elemento 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 Mockito verify che lo rende un modello fittizio: puoi confermare il modello fittizio navController chiamato metodo specifico (navigate) con un parametro (actionTasksFragmentToTaskDetailFragment con ID "quo1";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 breve, per testare la navigazione puoi:

  1. Usa Mockito per creare una simulazione di NavController.
  2. Allega la simulazione del NavController al frammento.
  3. Verifica che la navigazione sia stata chiamata con l'azione e i parametri corretti.

Passaggio 3. Facoltativo: scrivi clickAddTaskButton_browsingToAddEditFragment

Prova a eseguire questa attività per verificare di poter scrivere autonomamente un test di navigazione.

  1. Scrivi il test clickAddTaskButton_navigateToAddEditFragment, che verifica che, se fai clic sul pulsante FAB +, accedi alla AddEditTaskFragment.

La risposta è la seguente.

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 che hai iniziato e quello finale.

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

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


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

Scarica Zip

Questo codelab ha spiegato come configurare l'inserimento manuale delle dipendenze, un localizzatore di servizi e come utilizzare falsi e simulazioni nelle tue app Android Kotlin. In particolare:

  • Gli elementi che vuoi testare e la strategia di test determinano quali tipi di test implementerai per la tua app. I test delle unità sono incentrati e veloci. I test di integrazione verificano l'interazione tra le parti del programma. I test end-to-end verificano le funzionalità, registrano il livello di fedeltà più elevato, sono spesso strumentati e la loro esecuzione può richiedere più tempo.
  • L'architettura della tua app influisce sulla difficoltà di test.
  • La strategia TDD o Test Driven Development è una strategia in cui scrivi prima i test e poi crea la funzionalità per superarli.
  • Per isolare parti della tua app ai fini del test, puoi utilizzare i doppi del test. Un doppio test è una versione di un corso creato appositamente per i test. Ad esempio, falsifichi di ottenere dati da un database o da Internet.
  • Usa l'inserimento dipendenza per sostituire una classe reale con una classe di test, ad esempio un repository o un livello di networking.
  • Utilizza i test del sistema (androidTest) per avviare i componenti dell'interfaccia utente.
  • Quando non puoi utilizzare l'inserimento di dipendenze del costruttore, ad esempio per lanciare un frammento, spesso puoi utilizzare un Service Locator. Il Pattern Service Locator è un'alternativa all'iniezione di dipendenza. Implica la creazione di una classe singleton denominata "Locator di servizio", il cui scopo è fornire dipendenze, sia per il codice normale sia per il codice di test.

Corso Udacity:

Documentazione per gli sviluppatori Android:

Video:

Altro:

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