Stabilità dei test di grandi dimensioni

La natura asincrona di framework e applicazioni mobile spesso complica la scrittura di 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ò variare dalla modifica di un testo sullo schermo alla ricostruzione 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 problemi quando esegui operazioni asincrone o in background sconosciute al test, ad esempio il caricamento di dati da un database o la visualizzazione di animazioni infinite.

diagramma di flusso che mostra un ciclo che controlla se l'app è inattiva prima di eseguire un passaggio di 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 a cui puoi eseguire query per verificare l'inattività o che migliorano la sincronizzazione, ad esempio TestDispatcher per le 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 misure principali che puoi adottare per ridurre la variabilità 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 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 per 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. Anche se devi sempre monitorare e correggere i test inaffidabili, un meccanismo di ripetizione può contribuire a mantenere la produttività degli sviluppatori eseguendo il test più volte finché non viene superato.

I nuovi tentativi devono essere eseguiti a più livelli 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.