Utilizzare il doppio del test in Android

Quando progetti la strategia di test per un elemento o un sistema, esistono tre aspetti correlati:

  • Ambito: quanta parte del codice viene toccata dal test? I test possono verificare un singolo metodo, l'intera applicazione o una via intermedia. L'ambito testato è in fase di test e in genere viene definito Oggetto in fase di test, ma anche come Sistema in fase di test o Unità sottoposta a test.
  • Velocità: quanto è veloce il test? Le velocità di test possono variare da millisecondi a diversi minuti.
  • Fedeltà: quanto è "reale" il test? Ad esempio, se una parte del codice che stai testando deve effettuare una richiesta di rete, il codice di test effettua effettivamente questa richiesta di rete o simula il risultato? Se il test comunica effettivamente con la rete, significa che ha una fedeltà superiore. Il compromesso è che l'esecuzione del test potrebbe richiedere più tempo, che potrebbe causare errori se la rete non è disponibile o che potrebbe essere costoso da utilizzare.

Scopri quali elementi verificare per scoprire come iniziare a definire la tua strategia di test.

Isolamento e dipendenze

Quando testi un elemento o un sistema di elementi, lo fai in isolamento. Ad esempio, per testare un ViewModel non è necessario avviare un emulatore e avviare una UI perché non dipende (o non dovrebbe) dal framework Android.

Tuttavia, il suo funzionamento potrebbe dipendere da altri. Ad esempio, per funzionare un ViewModel potrebbe dipendere da un repository di dati.

Quando devi fornire una dipendenza a un soggetto sottoposto a test, una pratica comune consiste nel creare un oggetto di test o doppio di test. I doppi di test sono oggetti che guardano e agiscono da componenti dell'app, ma sono creati nel tuo test per fornire dati o comportamenti specifici. I vantaggi principali sono che rendono i test più semplici e veloci.

Tipi di test raddoppiati

Esistono vari tipi di test doppi:

Falso Un test double che ha un'implementazione "funzionante" della classe, ma che la rende valida per i test ma non adatta alla produzione.

Esempio: un database in memoria.

I falsi non richiedono uno schema di simulazione e sono leggeri. Sono preferiti.

Finta Un test duplicato che comporta il comportamento della programmazione e che ha aspettative sulle sue interazioni. Le simulazioni non supereranno i test se le loro interazioni non corrispondono ai requisiti da te definiti. Le simulazioni vengono generalmente create con un framework fittizio per raggiungere questo obiettivo.

Esempio: verifica che un metodo in un database sia stato chiamato esattamente una volta.

Gambo Un test double che si comporta come lo programmi, ma non ha aspettative sulle sue interazioni. Solitamente creato con un modello di simulazione. Per semplicità, preferiamo i falsi alla stub.
dummy Un test duplicato che viene passato ma non viene utilizzato, ad esempio se devi solo fornirlo come parametro.

Esempio: una funzione vuota passata come callback di un clic.

Spionaggio Un wrapper su un oggetto reale che tiene anche traccia di alcune informazioni aggiuntive, come per le simulazioni. In genere vengono evitate per una maggiore complessità. I falsi o le simulazioni sono quindi da preferire alle spie.
Ombre Falso utilizzato in Robolectric.

Esempio di utilizzo di un falso

Supponi di voler eseguire il test delle unità di un ViewModel che dipende da un'interfaccia denominata UserRepository ed espone il nome del primo utente a una UI. Puoi creare un test falso implementando l'interfaccia e restituendo i dati noti.

object FakeUserRepository : UserRepository {
    fun getUsers() = listOf(UserAlice, UserBob)
}

val const UserAlice = User("Alice")
val const UserBob = User("Bob")

Questo UserRepository fittizio non deve dipendere dalle origini dati locali e remote che utilizzerebbe la versione di produzione. Il file si trova nel set di origini di test e non verrà spedito con l'app di produzione.

Una dipendenza falsa può restituire dati noti senza dipendere da origini dati remote
Figura 1: una dipendenza finta in un test delle unità.

Il test seguente verifica che ViewModel mostri correttamente il primo nome utente nella vista.

@Test
fun viewModelA_loadsUsers_showsFirstUser() {
    // Given a VM using fake data
    val viewModel = ViewModelA(FakeUserRepository) // Kicks off data load on init

    // Verify that the exposed data is correct
    assertEquals(viewModel.firstUserName, UserAlice.name)
}

Sostituire UserRepository con un falso è facile in un test delle unità, perché il ViewModel è creato dal tester. Tuttavia, può essere difficile sostituire gli elementi arbitrari nei test più estesi.

Sostituzione dei componenti e inserimento delle dipendenze

Quando i test non hanno alcun controllo sulla creazione dei sistemi sottoposti a test, la sostituzione dei componenti per i test doppi diventa più complessa e richiede che l'architettura dell'app sia conforme a un design testabile.

Anche i test end-to-end di grandi dimensioni possono trarre vantaggio dall'uso di doppi test, ad esempio un test dell'interfaccia utente strumentale che naviga attraverso un flusso utente completo nella tua app. In questo caso, è consigliabile rendere il test ermetico. Un test ermetico evita tutte le dipendenze esterne, come il recupero dei dati da internet. Questo migliora l'affidabilità e le prestazioni.

Figura 2: un grande test che copre la maggior parte dell'app e falsifica i dati remoti.

Puoi progettare l'app per ottenere questa flessibilità manualmente, ma ti consigliamo di utilizzare un framework di inserimento delle dipendenze come Hilt per sostituire i componenti dell'app al momento del test. Consulta la guida al test dell'hilt.

Robolectric

Su Android, puoi utilizzare il framework Robolectric, che offre un tipo speciale di test doppio. Robolectric consente di eseguire i test sulla workstation o sull'ambiente di integrazione continua. Utilizza una JVM normale, senza emulatore o dispositivo. Simula l'aumento artificiale delle visualizzazioni, il caricamento delle risorse e altre parti del framework Android con duplicati di test chiamati shadow.

Robolectric è un simulatore, pertanto non deve sostituire semplici test delle unità né essere utilizzato per eseguire test di compatibilità. Garantisce velocità e riduce i costi, a scapito di una fedeltà inferiore, in alcuni casi. Un buon approccio per i test dell'interfaccia utente è renderli compatibili con i test Robolectric e strumentati e decidere quando eseguirli a seconda della necessità di testare la funzionalità o la compatibilità. Entrambi i test Espresso e Compose possono essere eseguiti su Robolectric.