Stabilità dei test di grandi dimensioni

La natura asincrona di framework e applicazioni mobile spesso rende difficile scrivere test affidabili e ripetibili. Quando viene inserito un evento utente, il framework di test deve attendere che l'app finisca di reagire, il che può includere la modifica di un testo sullo schermo o la ricreazione completa di un'attività. Quando un test non ha un comportamento deterministico, è incostante.

I framework moderni come Compose o Espresso sono progettati tenendo conto dei test, pertanto c'è una certa garanzia che l'interfaccia utente sarà inattiva prima dell'azione o dell'affermazione di test successiva. Questa è la sincronizzazione.

Testare la sincronizzazione

Possono comunque verificarsi dei problemi quando esegui operazioni asincrone o in background sconosciute al test, come il caricamento di dati da un database o la visualizzazione di animazioni infinite.

diagramma di flusso che mostra un loop che controlla se l'app è inattiva prima di superare il test
Figura 1: sincronizzazione del test.

Per aumentare l'affidabilità della suite di test, puoi installare un modo per monitorare le operazioni in background, ad esempio Espresso Idling Resources. Inoltre, puoi sostituire i moduli per le versioni di test su cui puoi eseguire query per inattività o che migliorare la sincronizzazione, ad esempio TestDispatcher per coroutine o RxIdler per RxJava.

Diagramma che mostra un errore di test quando la sincronizzazione si basa sull'attesa di un orario prestabilito
Figura 2: l'utilizzo del sonno nei test comporta test lenti o incostanti.

Modi per migliorare la stabilità

I test di grandi dimensioni possono rilevare molte regressioni contemporaneamente perché testano diversi componenti di un'app. In genere vengono eseguiti su emulatori o dispositivi, il che significa che hanno un'elevata fedeltà. Sebbene i test end-to-end di grandi dimensioni forniscano una copertura completa, sono più soggetti a errori occasionali.

Le principali misure che puoi adottare per ridurre l'instabilità sono le seguenti:

  • Configurare correttamente i dispositivi
  • Evitare problemi di sincronizzazione
  • Implementare i tentativi

Per creare test di grandi dimensioni utilizzando Compose o Espresso, in genere avvii una delle tue attività e navighi come farebbe un utente, verificando che l'UI si comporti correttamente utilizzando asserzioni o test di screenshot.

Altri framework, come UI Automator, consentono un ambito più ampio, in quanto puoi interagire con l'interfaccia utente di sistema e con altre app. Tuttavia, i test di UI Automator potrebbero richiedere una maggiore sincronizzazione manuale, pertanto tendono a essere meno affidabili.

Configurare i dispositivi

Innanzitutto, per migliorare l'affidabilità dei test, devi assicurarti che il sistema operativo del dispositivo non interrompa inaspettatamente l'esecuzione dei test. Ad esempio, quando una finestra di dialogo di aggiornamento di sistema viene visualizzata sopra altre app o quando lo spazio su disco non è sufficiente.

I fornitori di farm di dispositivi configurano i propri dispositivi ed emulatori, quindi in genere non devi fare nulla. Tuttavia, potrebbero avere le proprie direttive di configurazione per i casi speciali.

Dispositivi gestiti da Gradle

Se gestisci personalmente gli emulatori, puoi utilizzare Dispositivi gestiti da Gradle per definire i dispositivi da utilizzare per eseguire i test:

android {
  testOptions {
    managedDevices {
      localDevices {
        create("pixel2api30") {
          // Use device profiles you typically see in Android Studio.
          device = "Pixel 2"
          // Use only API levels 27 and higher.
          apiLevel = 30
          // To include Google services, use "google".
          systemImageSource = "aosp"
        }
      }
    }
  }
}

Con questa configurazione, il seguente comando creerà un'immagine dell'emulatore, avvia un'istanza, esegue i test e la arresta.

./gradlew pixel2api30DebugAndroidTest

I dispositivi gestiti da Gradle contengono meccanismi che consentono di riprovare in caso di disconnessioni del dispositivo e altri miglioramenti.

Evitare problemi di sincronizzazione

I componenti che eseguono operazioni in background o asincrone possono causare errori di test perché un'istruzione di test è stata eseguita prima che l'interfaccia utente fosse pronta. Man mano che l'ambito di un test aumenta, aumentano le probabilità che diventi instabile. Questi problemi di sincronizzazione sono una fonte principale di instabilità perché i framework di test devono dedurre se il caricamento di un'attività è terminato o se deve attendere più a lungo.

Soluzioni

Puoi utilizzare le risorse inattive di Espresso per indicare quando un'app è occupata, ma è difficile monitorare ogni operazione asincrona, in particolare in test end-to-end molto grandi. Inoltre, le risorse inattive possono essere difficili da installare senza contaminare il codice in test.

Anziché stimare se un'attività è occupata o meno, puoi fare in modo che i test aspettino fino a quando non vengono soddisfatte condizioni specifiche. Ad esempio, puoi attendere fino a quando un testo o un componente specifico non viene visualizzato nell'interfaccia utente.

Compose dispone di una raccolta di API di test nell'ambito della ComposeTestRule per attendere diversi corrispondenti:

fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilExactlyOneExists(matcher: SemanticsMatcher,  timeout: Long = 1000L)

fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)

E un'API generica che accetta qualsiasi funzione che restituisce un valore booleano:

fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit

Esempio di utilizzo:

composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>

Meccanismi di ripetizione

Dovresti correggere i test inaffidabili, ma a volte le condizioni che ne causano il fallimento sono così improbabili che sono difficili da riprodurre. Sebbene sia necessario tenere sempre traccia e correggere i test irregolari, un meccanismo di ripetizione può aiutare a mantenere la produttività degli sviluppatori eseguendo il test un certo numero di volte fino a quando non viene superato.

Per evitare problemi, ad esempio:

  • Connessione al dispositivo scaduta o persa
  • Test singolo non riuscito

L'installazione o la configurazione dei tentativi dipende dai framework di test e dall'infrastruttura, ma i meccanismi tipici includono:

  • Una regola JUnit che riprova qualsiasi test un certo numero di volte
  • Un passaggio o un'azione di ripetizione nel flusso di lavoro CI
  • Un sistema per riavviare un emulatore quando non risponde, ad esempio i dispositivi gestiti da Gradle.