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:
- Il linguaggio di programmazione Kotlin
- Concetti di base trattati nel primo codelab: scrittura ed esecuzione di test delle unità su Android, utilizzando JUnit, Hamcrest, test di AndroidX, Robolectric e test di LiveData
- Le seguenti librerie principali di Android Jetpack:
ViewModel
,LiveData
e il componente di navigazione - Architettura dell'applicazione, seguendo lo schema indicato nella Guida all'architettura delle app e i codelab di Android Fundamentals.
- Le basi della coroutine su Android
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:
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:
- Stanza con un codelab sulla visualizzazione
- Codelab di formazione su Android Kotlin Fundamentals
- Codelab sulla formazione Android avanzata
- Esempio di girasole per Android
- Sviluppo di app Android con il corso di formazione Kotlin Udacity
È 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: | |
| La schermata Aggiungi o modifica un'attività: codice dell'interfaccia utente per aggiungere o modificare un'attività. |
| Livello dati:include il livello dati delle attività. Contiene il codice del database, della rete e del repository. |
| Schermata delle statistiche: codice del livello dell'interfaccia utente per la schermata delle statistiche. |
| Schermata Dettagli attività: codice del livello UI per una singola attività. |
| Schermata Attività: codice del livello UI per l'elenco di tutte le attività. |
| 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:
- Innanzitutto eseguirai il test dell'unità repository.
- Quindi, utilizzerai un modello di test doppio nel modello vista, che è necessario per eseguire i test delle unità e l'integrazione del modello vista.
- Dopodiché imparerai a scrivere test di integrazione per frammenti e relativi modelli di visualizzazione.
- 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 |
Fumetto | Un test doppio che viene trasmesso ma non utilizzato, ad esempio se devi solo specificare come parametro. Se avessi un elemento |
Spia | Un doppio test che tiene traccia anche di alcune informazioni aggiuntive; ad esempio, se hai creato un |
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
.
- Nel set di origine di test, fai clic con il pulsante destro del mouse e seleziona Nuovo pacchetto ->.
- Crea un pacchetto di dati con un pacchetto di origine all'interno.
- 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
.
- 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 { ... }
- Fai in modo che
FakeDataSource
implementiTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
Android Studio spiega che non hai implementato i metodi richiesti per TasksDataSource
.
- Utilizza il menu a risoluzione 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 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.
- Modifica il costruttore
FakeDataSource
per creare unvar
denominatotasks
che sia unMutableList<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:
- Scrittura
getTasks
: setasks
non ènull
, restituisci un risultatoSuccess
. Setasks
ènull
, restituisci un risultatoError
. - Scrivere
deleteAllTasks
: cancella l'elenco delle attività modificabili. - 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
- Cambia la scelta del costruttore di
DefaultTaskRepository
per assumere unApplication
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 }
- Poiché hai passato le dipendenze, rimuovi il metodo
init
. Non devi più creare le dipendenze. - 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
- 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
.
- Fai clic con il pulsante destro del mouse sul nome della classe
DefaultTasksRepository
e seleziona Genera, quindi Test. - Segui le istruzioni per creare
DefaultTasksRepositoryTest
nel set di origini test. - 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 }
- Crea tre variabili, due
FakeDataSource
per i membri (una per ciascuna origine dati per il repository) e una variabile perDefaultTasksRepository
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
.
- Crea un metodo chiamato
createRepository
e annotalo con@Before
. - Crea un'istanza per le tue origini dati false utilizzando gli elenchi
remoteTasks
elocalTasks
. - Crea un'istanza di
tasksRepository
utilizzando le due origini dati false che hai appena creato eDispatchers.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
.
- Scrivi un test per il metodo
getTasks
del repository. Quando chiamigetTasks
contrue
, 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.
- 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.
- 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. - Torna alla
DefaultTasksRepositoryTest
, aggiungirunBlockingTest
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))
}
}
- 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
.
- Apri
DefaultTasksRepository
e fai clic con il pulsante destro del mouse sul nome del corso. quindi seleziona Refactoring -> Estrai -> interfaccia.
- Scegli Estrai in un file separato.
- Nella finestra Estrai interfaccia, modifica il nome dell'interfaccia in
TasksRepository
. - Nella sezione Membri da compilare, seleziona tutti i membri tranne i due membri companion e i metodi privati.
- Fai clic su Refactoring. La nuova interfaccia
TasksRepository
dovrebbe apparire nel pacchetto data/source.
E ora DefaultTasksRepository
implementa TasksRepository
.
- 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
.
- Nel set di origini test, in data/source crea il file Kotlin e la classe
FakeTestRepository.kt
ed estendi dall'interfaccia diTasksRepository
.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
Ti verrà chiesto di implementare i metodi dell'interfaccia.
- Passa il mouse sopra l'errore fino a visualizzare il menu dei suggerimenti, quindi fai clic e seleziona Implementa membri.
- 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.
- In
FakeTestRepository
, aggiungi sia una variabileLinkedHashMap
che rappresenti l'elenco corrente di attività sia unaMutableLiveData
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:
getTasks
: questo metodo dovrebbe prendere iltasksServiceData
e trasformarlo in un elenco utilizzandotasksServiceData.values.toList()
e poi restituire il risultato comeSuccess
.refreshTasks
: aggiorna il valoreobservableTasks
in modo che sia quello restituito dagetTasks()
.observeTasks
: crea una coroutine utilizzandorunBlocking
ed eseguirefreshTasks
, quindi restituisceobservableTasks
.
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à.
- Aggiungi il metodo
addTasks
, che prevede unvararg
di attività, li aggiunge aHashMap
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
.
- Apri
TasksViewModel
. - Cambia il costruttore di
TasksViewModel
in modo che accettiTasksRepository
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.
- Nella parte inferiore del file
TasksViewModel
, fuori dal corso, aggiungi unTasksViewModelFactory
che contenga un sempliceTasksRepository
.
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.
- 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))
}
- 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.
- Apri
TasksViewModelTest
. - Aggiungi una proprietà
FakeTestRepository
inTasksViewModelTest
.
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
setupViewModel
per creare unFakeTestRepository
con tre attività, quindi costruisci iltasksViewModel
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)
}
- Poiché non utilizzi più il codice AndroidX Test
ApplicationProvider.getApplicationContext
, puoi anche rimuovere l'annotazione@RunWith(AndroidJUnit4::class)
. - 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.
- Apri
TaskDetailViewModel
. - 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 }
- In fondo al file
TaskDetailViewModel
, all'esterno del corso, aggiungi unTaskDetailViewModelFactory
.
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)
}
- 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))
}
- 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
- 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 principalekotlinx-coroutines-test
: la libreria per i test delle coroutineandroidx.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.
- Apri
taskdetail.TaskDetailFragment
. - 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 fontitest
).
- 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
).
- 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 elementoFragmentScenario
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.
- Questo è un test strumentale, quindi assicurati che l'emulatore o il tuo dispositivo sia visibile.
- 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:
- Creare una classe Service Locator in grado di creare e archiviare un repository. Per impostazione predefinita, crea un repository "normal".
- Esegui il refactoring del codice in modo che, quando è necessario un repository, utilizza Service Locator.
- 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.
- Crea il file ServiceLocator.kt nel livello superiore del set di origine principale.
- Definisci un
object
chiamatoServiceLocator
. - Crea variabili di istanza
database
erepository
e imposta entrambe sunull
. - 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:
provideTasksRepository
: fornisce un repository già esistente o ne crea uno nuovo. Questo metodo dovrebbe esseresynchronized
suthis
per evitare, in situazioni con più thread in esecuzione, di creare accidentalmente due istanze di repository.createTasksRepository
: codice per la creazione di un nuovo repository. ChiameràcreateTaskLocalDataSource
e creerà un nuovoTasksRemoteDataSource
.createTaskLocalDataSource
: codice per la creazione di una nuova origine dati locale. Chiamerà 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. 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.
- Al livello più alto della gerarchia di pacchetti, apri
TodoApplication
e crea unval
per il repository, quindi 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
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
.
- Apri
TaskDetailFragement
e trova la chiamata al numerogetRepository
nella parte superiore del corso. - 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)
}
- 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)
}
- Per
StatisticsViewModel
eAddEditTaskViewModel
, aggiorna il codice che acquisisce il repository per utilizzarlo daTodoApplication
.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- 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
.
- 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. - Crea un nuovo corso in questo pacchetto di origine chiamato
FakeAndroidTestRepository.kt
. - 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
.
- Apri
ServiceLocator.kt
. - 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.
- Aggiungi una variabile di istanza denominata
lock
con il valoreAny
.
ServiceLocator.kt
private val lock = Any()
- 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
.
- Apri
TaskDetailFragmentTest
. - Dichiara una variabile
lateinit TasksRepository
. - 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()
}
- Aggrega il corpo della funzione
activeTaskDetails_DisplayedInUi()
inrunBlockingTest
. - 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)
}
- 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)
}
}
- 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!):
- Sul dispositivo di test, vai a Impostazioni.
- 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:
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
.
- Apri
TaskDetailFragmentTest.kt
. - 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
- Tutto ciò che segue il commento di
// THEN
utilizza Espresso. Esamina la struttura dei test e l'uso diwithId
e controlla se l'aspetto della pagina dei dettagli deve essere corretto. - Esegui il test e verifica che superi.
Passaggio 4. (Facoltativo) Scrivi il tuo Espresso Test
Ora scrivi un test in autonomia.
- 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
}
- Esaminando il test precedente, completalo.
- 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
- 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, comeDatePicker
eRecyclerView
. Contiene inoltre controlli di accessibilità e una classe denominataCountingIdlingResource
, trattata in un secondo momento.
Passaggio 2. Crea TasksFragmentTest
- Apri
TasksFragment
. - 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. - 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.
- 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)
}
- 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.
- Rendi il tuo nuovo modello fittizio il
NavController
del frammento.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- 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.
- 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")
)
}
- Esegui il test.
In breve, per testare la navigazione puoi:
- Usa Mockito per creare una simulazione di
NavController
. - Allega la simulazione del
NavController
al frammento. - 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.
- Scrivi il test
clickAddTaskButton_navigateToAddEditFragment
, che verifica che, se fai clic sul pulsante FAB +, accedi allaAddEditTaskFragment
.
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.
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:
- Guida all'architettura delle app
runBlocking
erunBlockingTest
FragmentScenario
- Espresso
- Mockito
- JUnit4
- Libreria di test di AndroidX
- Libreria di test per i componenti dell'architettura AndroidX
- Set di origini
- Testa dalla riga di comando
Video:
Altro:
Per i link ad altri codelab in questo corso, consulta la pagina di destinazione Advanced Android in Kotlin.