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:
- Il linguaggio di programmazione Kotlin
- Concetti di test trattati nel primo codelab: scrittura ed esecuzione di test delle unità su Android, utilizzando JUnit, Hamcrest, AndroidX Test, Robolectric e Testing LiveData
- Le seguenti librerie Android Jetpack principali:
ViewModel,LiveDatae il componente di navigazione - Architettura dell'applicazione, seguendo il pattern della Guida all'architettura delle app e dei codelab Android Fundamentals
- Nozioni di base sulle coroutine su Android
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:
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:
- Codelab Room with a View
- Codelab del corso di formazione sui concetti fondamentali di Kotlin per Android
- Codelab di formazione avanzata per Android
- Esempio di Android Sunflower
- Developing Android Apps with Kotlin Udacity training course
È 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: | |
| La schermata Aggiungi o modifica un'attività:codice del livello UI per aggiungere o modificare un'attività. |
| Il livello dati:riguarda il livello dati delle attività. Contiene il codice del database, della rete e del repository. |
| Schermata delle statistiche:codice del livello UI per la schermata delle statistiche. |
| Schermata dei dettagli dell'attività:codice del livello UI per una singola attività. |
| Schermata delle attività:codice del livello UI per l'elenco di tutte le attività. |
| 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:
- Per prima cosa, esegui il test unitario del repository.
- Poi utilizzerai un test double nel modello di visualizzazione, necessario per il test delle unità e il test di integrazione del modello di visualizzazione.
- Successivamente, imparerai a scrivere test di integrazione per fragment e i relativi modelli di visualizzazione.
- 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 |
Dummy | Un test double che viene passato ma non utilizzato, ad esempio se devi solo fornirlo come parametro. Se avessi un |
Spy | Un test double che tiene traccia anche di alcune informazioni aggiuntive. Ad esempio, se hai creato un |
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.
- Nel set di origini test, fai clic con il tasto destro del mouse e seleziona Nuovo > Pacchetto.

- Crea un pacchetto data con un pacchetto source all'interno.
- Crea una nuova classe denominata
FakeDataSourcenel 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.

- 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 { ... }- Fai in modo che
FakeDataSourceimplementiTasksDataSource:
class FakeDataSource : TasksDataSource {
}Android Studio segnalerà che non hai implementato i metodi richiesti per TasksDataSource.
- Utilizza il menu di correzione rapida e seleziona Implementa membri.

- 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
DefaultTasksRepositorysenza dover fare affidamento su un database o una rete reali. - fornisce un'implementazione "abbastanza reale" per i test.
- Modifica il costruttore
FakeDataSourceper creare unvarchiamatotasksche sia unMutableList<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:
- Scrivi
getTasks: setasksnon ènull, restituisci un risultatoSuccess. Setasksènull, restituisci un risultatoError. - Scrivi
deleteAllTasks: cancella l'elenco delle attività modificabili. - 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.TaskQuesto è 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
- Modifica il costruttore di
DefaultTaskRepositoryin 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 }- Poiché hai passato le dipendenze, rimuovi il metodo
init. Non è più necessario creare le dipendenze. - 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- Infine, aggiorna il metodo
getRepositoryper 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.
- Fai clic con il tasto destro del mouse sul nome della classe
DefaultTasksRepositorye seleziona Genera, poi Testa. - Segui le istruzioni per creare
DefaultTasksRepositoryTestnel set di origini test. - 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 }- Crea tre variabili: due variabili membro
FakeDataSource(una per ogni origine dati del repository) e una variabile perDefaultTasksRepositoryche testerai.
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepositoryCrea un metodo per configurare e inizializzare un DefaultTasksRepository testabile. Questo DefaultTasksRepository utilizzerà il tuo test double, FakeDataSource.
- Crea un metodo denominato
createRepositorye annotalo con@Before. - Crea un'istanza delle origini dati fittizie utilizzando gli elenchi
remoteTaskselocalTasks. - Crea un'istanza di
tasksRepositoryutilizzando le due origini dati fittizie che hai appena creato eDispatchers.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.
- Scrivi un test per il metodo
getTasksdel repository. Verifica che quando chiamigetTaskscontrue(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.
- 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.
- Aggiungi
@ExperimentalCoroutinesApisopra la classe. Ciò indica che sai di utilizzare un'API coroutine sperimentale (runBlockingTest) nella classe. In caso contrario, riceverai un avviso. - Torna a
DefaultTasksRepositoryTeste aggiungirunBlockingTestin 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))
}
}- Esegui il nuovo test
getTasks_requestsAllTasksFromRemoteDataSourcee 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.
- Apri
DefaultTasksRepositorye fai clic con il tasto destro del mouse sul nome del corso. Quindi seleziona Refactor -> Extract -> Interface.

- Scegli Estrai in un file separato.

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

- Fai clic su Refactor. La nuova interfaccia
TasksRepositorydovrebbe essere visualizzata nel pacchetto data/source .

e DefaultTasksRepository ora implementa TasksRepository.
- 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.
- Nel set di origini test, in data/source crea il file Kotlin e la classe
FakeTestRepository.kted estendili dall'interfacciaTasksRepository.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}Ti verrà comunicato che devi implementare i metodi dell'interfaccia.
- Passa il mouse sopra l'errore finché non visualizzi il menu dei suggerimenti, quindi fai clic e seleziona Implementa membri.
- 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.
- In
FakeTestRepository, aggiungi sia una variabileLinkedHashMapche rappresenta l'elenco corrente delle attività sia unMutableLiveDataper 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:
getTasks: questo metodo deve prenderetasksServiceData, trasformarlo in un elenco utilizzandotasksServiceData.values.toList()e restituirlo come risultatoSuccess.refreshTasks: aggiorna il valore diobservableTasksin modo che corrisponda a quello restituito dagetTasks().observeTasks: crea una coroutine utilizzandorunBlockinged eseguirefreshTasks, quindi restituisceobservableTasks.
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à.
- Aggiungi il metodo
addTasks, che accetta unvarargdi attività, aggiunge ciascuna alHashMape 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.
- Apri
TasksViewModel. - Modifica il costruttore di
TasksViewModelin modo che accettiTasksRepositoryanziché 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.
- Nella parte inferiore del file
TasksViewModel, al di fuori della classe, aggiungi unTasksViewModelFactoryche accetta unTasksRepositorysemplice.
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.
- Aggiorna
TasksFragmentper utilizzare la fabbrica.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- 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.
- Apri
TasksViewModelTest. - Aggiungi una proprietà
FakeTestRepositoryinTasksViewModelTest.
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
}- Aggiorna il metodo
setupViewModelper creare unFakeTestRepositorycon tre attività, quindi creatasksViewModelcon 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)
}- Poiché non utilizzi più il codice AndroidX Test
ApplicationProvider.getApplicationContext, puoi anche rimuovere l'annotazione@RunWith(AndroidJUnit4::class). - 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.
- Apri
TaskDetailViewModel. - 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 }- In fondo al file
TaskDetailViewModel, al di fuori della classe, aggiungi unTaskDetailViewModelFactory.
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)
}- Aggiorna
TasksFragmentper utilizzare la fabbrica.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- 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
- 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 basekotlinx-coroutines-test: la libreria di test delle coroutineandroidx.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.
- Apri
taskdetail.TaskDetailFragment. - 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 originetest).

- 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à@SmallTeste 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).
- 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:
- Crea un'attività.
- Crea un
Bundle, che rappresenta gli argomenti del frammento per l'attività che vengono passati al frammento. - La funzione
launchFragmentInContainercrea unFragmentScenariocon questo bundle e un tema.
Questo non è ancora un test completo, perché non afferma nulla. Per ora, esegui il test e osserva cosa succede.
- Si tratta di un test strumentato, quindi assicurati che l'emulatore o il dispositivo sia visibile.
- 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:
- Crea una classe Service Locator in grado di costruire e archiviare un repository. Per impostazione predefinita, viene creato un repository "normale".
- Esegui il refactoring del codice in modo che quando hai bisogno di un repository, utilizzi il Service Locator.
- 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.
- Crea il file ServiceLocator.kt nel livello superiore del set di origini principale.
- Definisci un
objectchiamatoServiceLocator. - Crea le variabili di istanza
databaseerepositorye impostale entrambe sunull. - Annota il repository con
@Volatileperché 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:
provideTasksRepository: fornisce un repository già esistente o ne crea uno nuovo. Questo metodo deve esseresynchronizedsuthisper evitare, in situazioni con più thread in esecuzione, di creare accidentalmente due istanze del repository.createTasksRepository: codice per creare un nuovo repository. ChiameràcreateTaskLocalDataSourcee creerà un nuovoTasksRemoteDataSource.createTaskLocalDataSource: codice per creare una nuova origine dati locale. Verrà chiamato il numerocreateDataBase.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.
- Al livello superiore della gerarchia dei pacchetti, apri
TodoApplicatione crea unvalper il repository e assegnagli un repository ottenuto utilizzandoServiceLocator.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.
- Apri
DefaultTasksRepositoryed 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.
- Apri
TaskDetailFragemente trova la chiamata agetRepositorynella parte superiore della classe. - 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)
}- 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)
}- Per
StatisticsViewModeleAddEditTaskViewModel, aggiorna il codice che acquisisce il repository in modo che utilizzi il repository diTodoApplication.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- 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.
- Fai clic con il tasto destro del mouse sul set di origini
androidTeste crea un pacchetto di dati. Fai di nuovo clic con il tasto destro del mouse e crea un pacchetto sorgente . - Crea una nuova classe in questo pacchetto di origine chiamato
FakeAndroidTestRepository.kt. - 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.
- Apri
ServiceLocator.kt. - Contrassegna il setter per
tasksRepositorycome@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 setChe 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.
- Aggiungi una variabile di istanza chiamata
lockcon il valoreAny.
ServiceLocator.kt
private val lock = Any()- Aggiungi un metodo specifico per i test chiamato
resetRepositoryche 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.
- Apri
TaskDetailFragmentTest. - Dichiara una variabile
lateinit TasksRepository. - Aggiungi un metodo di configurazione e smontaggio per configurare un
FakeAndroidTestRepositoryprima 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()
}
- Racchiudi il corpo della funzione
activeTaskDetails_DisplayedInUi()inrunBlockingTest. - Salva
activeTasknel 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)
}- 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)
}
}
- 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):
- Sul dispositivo di test, vai a Impostazioni > Opzioni sviluppatore.
- 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:
onViewonView è 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.
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.
- Apri
TaskDetailFragmentTest.kt. - 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- Tutto ciò che segue il commento
// THENutilizza Espresso. Esamina la struttura del test e l'utilizzo diwithIde verifica di poter fare asserzioni sull'aspetto della pagina dei dettagli. - Esegui il test e conferma che venga superato.
Passaggio 4: (Facoltativo) Scrivi il tuo test espresso
Ora scrivi un test.
- Crea un nuovo test chiamato
completedTaskDetails_DisplayedInUie 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
}- Esaminando il test precedente, completa questo test.
- 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
- 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, comeDatePickereRecyclerView. Contiene anche controlli di accessibilità e una classe chiamataCountingIdlingResourceche verrà trattata in un secondo momento.
Passaggio 2: Crea TasksFragmentTest
- Apri
TasksFragment. - Fai clic con il tasto destro del mouse sul nome della classe
TasksFragmente seleziona Genera, quindi Testa. Crea un test nel set di origini androidTest. - 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.
- 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)
}
- Utilizza la funzione
mockdi 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.
- Rendi il nuovo mock l'
NavControllerdel fragmento.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}- Aggiungi il codice per fare clic sull'elemento in
RecyclerViewche 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.
- Verifica che
navigatesia 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")
)
}- Esegui il test.
In sintesi, per testare la navigazione puoi:
- Utilizza Mockito per creare un mock
NavController. - Allega il
NavControllersimulato al fragmento. - 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à.
- Scrivi il test
clickAddTaskButton_navigateToAddEditFragmentche verifica che se fai clic sul pulsante di azione rapida +, si apre la schermataAddEditTaskFragment.
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.
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:
- Guida all'architettura delle app
runBlockingerunBlockingTestFragmentScenario- Espresso
- Mockito
- JUnit4
- Libreria di test AndroidX
- Libreria di test principale dei componenti dell'architettura AndroidX
- Set di fonti
- Test dalla riga di comando
Video:
Altro:
Per i link ad altri codelab di questo corso, consulta la pagina di destinazione dei codelab Advanced Android in Kotlin.




