Seleziona una pagina
Android testing: descrizione operativa

In questo post voglio parlare del testing su Android e in particolare degli end to end test, per Android anche detti ui test e perché progressivamente ne stiamo facendo decisamente molti di meno che non in passato.

Diciamo che a prescindere da quello che è un progetto, sia web ma soprattutto mobile, è una scelta un po’ inevitabile. Una trattazione un po’ più in dettaglio infatti la si può trovare nel sito di Martin Fowler nel famoso Test Pyramid. E’ anche interessante analizzare il pattern del Testing Trophy per capire perché può aver comunque molto senso avere vari test in Roboletric e non solo principalmente test unitari.

Storia del testing su Android

storia del testing di Android

Le app per cellulari inizialmente sono partite come app veramente “banali” e con poche ambizioni. Raramente trovavi lavori nel 2011-2012 dove le app mobile avevano un’importanza rilevante nell’economia del prodotto. Specialmente in Italia.

Per questo motivo e anche per via della sostanziale immaturità della piattaforma, il testing è sempre stato molto difficile se non virtualmente impossibile. Pensiamo al fatto che Espresso, il framework principale per fare ui test, arriva “solo” nell’ottobre 2013. Prima fare ui test era impossibile. E non è che a ottobre 2013 fosse tutto rose e fiori.

Per capirci, solo a febbraio 2015 fu possibile lanciare unit test dentro Android Studio. Ora può sembrare una banalità, ma fino a febbraio 2015 dovevi lanciarli con un emulatore o dispositivo online. Rendendo il tutto molto lento.

In buona sostanza, solo di recente fare testing su Android è diventato “umano”. Il che non vuol dire che il giorno dopo tutti lo facessero, visto che pochi erano abituati magari perché ex sviluppatori back-end.

Un’altra pietra miliare per il testing è sicuramente l’introduzione a maggio 2021, quindi sostanzialmente “ieri”, di Jetpack Compose. Questo toolkit permette di scrivere interfacce grafiche per Android in modo decisamente più semplice, riutilizzabile e anche senza dover usare l’XML. In questo modo scriviamo esclusivamente funzioni che possiamo sia riutilizzare ma anche testare in modo semplice. Chiamandole direttamente per vedere se l’output è ciò che ci aspettiamo.

In buona sostanza, prima di maggio 2021 il concetto di “testare unitariamente la view” era un concetto privo di significato. Ora invece, finalmente, è possibile.

Oserei dire che fino all’introduzione di Jetpack Compose l’unico modo era molto complesso. Ora finalmente si hanno tutti gli strumenti per farlo in un modo efficace e scalabile.

Ka/Espresso vs Roboletric vs Jetpack Compose

Ci sono vari strumenti per testare un’app Android. Vediamo quali sono i principali.

Espresso

quali flussi vengono testati con espresso
Espresso permette di testare end-to-end più schermate

E’ il framework di riferimento per Android per scrivere ui test ovvero test dove ci si finge un utente che preme fisicamente i bottoni e interagisce con la ui. Il linguaggio con cui è scritto è molto semplice da capire nel 90% dei casi.

codice di esempio di espresso

Il che, in realtà, è quasi uno svantaggio. E’ infatti molto più semplice capire come scrivere un buon test con Espresso che non uno unit test che testa lo stesso scenario.

Un altro pro e contro di Espresso, è che è facile fare dei test che “testano troppe cose”. Come un flusso quale “cancello un robot e verifico che nella home page non ci sono più i dati del robot”. In questo caso sto testando due cose. Sia la cancellazione del robot, che la “sparizione” del robot nella home page.

Questo porta questa tipologia di test a essere molto instabili (flacky) e anche molto difficili da mantenere in quanto è facile che qualcosa si rompa. E non sempre facile capire cosa di preciso.

Ti piacciono questi argomenti? Seguimi su instagram o Linkedin per non perdere i prossimi articoli 🙂

E’ naturale che si possono scrivere anche test più “puntuali”, tuttavia è una grande sfida. Da un lato la voglia di scrivere test piccoli, dall’altro il fatto che ogni “riavvio” di un test implica una notevole perdita di tempo e quindi conviene impilare più casistiche in una in modo da velocizzare l’esecuzione.

In buona sostanza sono facili da scrivere e da comprendere, ma sono una mazzata da mantenere. L’unico lato positivo è che testando il tutto end to end, se un test è verde, diciamo che sei molto vicino all’affidabilità di un test manuale.

Inoltre, un altro grande vantaggio è che vedi cosa succede quando il test gira e ti rendi conto visivamente di che problema c’è. In generale sono i test più “intuitivi” e anche quelli più semplici con cui iniziare

Kaspresso

logo kaspresso

Abbiamo provato a migrare su Kaspresso visto che la promessa era che non avremmo avuto più ui test flacky.

Abbiamo ancora ui test flacky.

Per cui boh… non so quanto vi convenga effettivamente usare Kaspresso. Va detto che la DSL di Kaspresso (ovvero Kakao) non è male. Per certi versi è più chiara di Espresso. Ma se dovessi tornare indietro, non so se rifarei la migrazione. I vantaggi non sono stati particolarmente significativi.

Roboletric

quali flussi vengono testati con roboletric
Con Roboletric posso fare test end-to-end a singola schermata

Abbiamo iniziato a usare Roboletric solo di recente quindi non mi sento di dire che siamo degli esperti. Tuttavia, con un po’ di effort, siamo riusciti a migrare molti dei test di Kaspresso su Roboletric. Anche mantenendo Kakao come DSL.

I vantaggi principali sono:

  • i test eseguono sulla JVM invece che su un device reale. Quindi sono notevolmente più veloci.
  • eseguendo su una JVM, è possibile farli anche eseguire sulla CI a ogni push sul repository.
  • è veramente complesso se non impossibile fare cambi di schermate. Il che ti obbliga a rendere i test “mono schermata” e anche abbastanza semplici.
  • non dovendo fare affidamento su un dispositivo fisico, sono un po’ meno flacky.

Lo svantaggio più grande è che non li vedi visto che girano sulla JVM. Inizialmente avevamo risolto il problema mettendoli in una cartella condivisa tra ui test e unit test. Il risultato è stato che hanno tolto il supporto delle shared folder da Android e questa cosa non si può più fare. Gran peccato.

Inoltre, può capitare che lo stesso test come ui test funziona mentre con Roboletric no. Capire come mai è spesso molto complesso e anche frustrante. Purtroppo capita più spesso del previsto con schermate in XML. Con schermate in Jetpack Compose invece è molto più difficile. Anche perché di fatto puoi usare i selettori di Compose.

In generale sono quindi più complessi da gestire, ma a livello di efficacia ed efficienza sono sicuramente superiori agli ui test veri e propri.

Jetpack Compose Testing

jetpack compose testa solo la view
Jetpack Compose permette di testare unitariamente la view

Il testing con Jetpack Compose è in generale davvero bello ed efficiente. Il grande vantaggio è che hai la sensazione di scrivere dei test unitari o quasi. E’ molto simile a Espresso quindi se hai già familiarità con Espresso non dovresti metterci poi molto a usare questo framework.

Devo dire che però il più grande difetto è che mancando totalmente gli id in compose, di fatto diventa un bel casino trovare gli elementi. Nel pratico, diventa tutto un “hasTestTag” e mettere testTag un po’ ovunque. Il che non è esattamente bellissimo. Diciamo che in generale lo fai proprio quando sei all’ultima spiaggia. Mentre con Jetpack Compose è un po’ la norma.

Di recente è stata introdotta Semantics che permette di dare un significato semantico a quanto viene visualizzato. Questo ne permette quindi anche un testing più semplice. Va detto però che non è così impossibile dare un significato semantico “sbagliato” e quindi testare una cosa ma visualizzarne un’altra. In linea generale però è un approccio sicuramente migliore di andare avanti a “testTag” a profusione 🙂

Allo stesso tempo stiamo mettendo sempre più codice in produzione a mero fine di test. Cosa che in generale sarebbe preferibile evitare. Tuttavia vista la struttura di Compose è un problema di difficile soluzione.

Tolto questo, i test sono generalmente affidabili, sono veloci, girano nella JVM, tutto molto bello. L’unica cosa è che appunto testi solo la UI quindi non stai testando il viewModel ed eventualmente altri layer sotto. Da un lato è un vantaggio, dall’altro però se si testa solo il viewModel e la view con Compose, non viene testata che l’integrazione funzioni. E non sempre le cose vanno come previsto.

integrazione vs unit test

C’è quindi il rischio di rilasciare qualche bug in produzione facendo solo unit test e test con Jetpack Compose?

il rischio c'è

Scherzi a parte, il mio consiglio è di lasciare il minimo di test degli happy path (ed eventuali error path) più critici come ui test (meglio se con Roboletric) e il resto testarlo unitariamente.

UIAutomator, Appium, altri

Ho usato raramente UIAutomator ma può tornare utile quando, per esempio, devi aprire i settings del telefono o comunque fare operazioni con il telefono. Come aprire la barra superiore e premere su una notifica. Queste cose sono fattibili con UIAutomator, girano se le metti su un ui test, ma scordatele su Roboletric 🙂

Ti piacciono questi argomenti? Seguimi su instagram o Linkedin per non perdere i prossimi articoli 🙂

Appium non l’ho mai usato personalmente ma lo usa un nostro SQA Engineer. Il grande vantaggio è che è cross platform. Il grande svantaggio è che è ancora più “end to end” dei test di Espresso. Quindi è ancora più facile avere test flacky e complessi da mantenere. Oltre a non poter interagire con elementi dell’app e quindi rende il testing ancora più complesso, specie se hai delle view custom.

Perché e cosa testare su Android?

perché e cosa testare su Android

Siamo al limite della domanda esistenziale, ma è davvero importante. In base anche al motivo di questa domanda, ci approcceremo al testing in modo totalmente differente.

Detta in altri termini, “non c’è il modo giusto di testare”. Ci sono approcci che possono funzionare meglio di altri, ma ci sono motivi legittimi per usare approcci totalmente differenti. La prima cosa da evitare è il voler partire subito riscrivendo tutto, come abbiamo già detto che è meglio non fare.

Se stiamo affrontando del codice legacy sarà inevitabile che, almeno inizialmente, avremmo molti più ui test di quelli che non vorremmo scrivere. Questo perché il codice sarà scritto probabilmente in modo non testabile ma allo stesso tempo, prima di toccarlo, vorremmo almeno preservare:

  • happy path, ovvero il percorso “base” della funzionalità che stiamo andando a toccare (presuppongo una sola funzionalità perché se stai facendo refactor di più di una funzionalità alla volta sei solo un pazzo suicida :-))
  • i più importanti error path e/o eventuali alternative all’happy path.

In questo modo almeno saremo tranquilli di non introdurre colossali regressioni. Inoltre, scrivere questi test ci obbligherà a capire quali erano i requisiti prima di andare a toccare il codice legacy. Sembra una cavolata, ma quante volte hai toccato codice senza avere il contesto necessario per farlo? Ogni volta che tocchi codice senza sufficiente contesto, introdurrai inevitabilmente dei bug.

Solo una volta che avremmo i vari percorsi verificati, potremmo iniziare a ragionare se c’è la possibilità e il margine per iniziare a introdurre degli unit-test sul ViewModel (si ma.. ce l’hai il ViewModel o neanche quello?) e quindi iniziare a togliere qualche ui test perché sei già “coperto” dai test del ViewModel. E anche capire se hai il tempo e il vantaggio di introdurre Jetpack Compose, in modo da smantellare anche qualche altro ui test.

Viceversa, se il codice è nuovo, puoi anche iniziare “solo” dai test della vista con Jetpack Compose, dai test del ViewModel e i test unitari di eventuali collaboratori o librerie che scrivi per l’app. Come ui test potresti scrivere un test “end to end” che verifichi che la funzionalità stia in piedi effettivamente. In molti casi tuttavia, se hai già gli altri test, potrebbe non essere strettamente necessario.

Infine… serve realmente che testi quell’app o quella funzionalità? Un caso che sembra raro ma che in realtà succede più spesso del previsto è quando dobbiamo fare del codice d’esempio. Un proof of concept o un’app di esempio. In quei casi, molte volte il testing non solo non è necessario, ma anzi andrebbe evitato per risparmiare tempo e soldi alla tua azienda.

Testiamo l’interazione con servizi esterni?

integrazione con i servizi esterni e Android testing

Un ultimo dubbio è se testare l’interazione con servizi esterni o avere uno strato di fake/mock che ce ne permette l’astrazione. Naturalmente negli unit test è una scelta obbligatoria, mentre negli ui test ha senso o meno?

A mio avviso è una scelta necessaria per via della troppa mutabilità dei servizi esterni. Inoltre, andremo a testare anche dei servizi esterni che, per definizione, andrebbero testati da chi ci espone tali servizi.

Naturalmente questo ragionamento vale anche nel caso ci interfacciassimo con dispositivi esterni come robot, dispositivi medicali o macchinari di vario genere.

Nel caso volessimo testare anche l’integrazione tra la nostra app e questi servizi esterni a mio avviso avrebbe più senso avere due flavor o due modalità per lanciare gli ui test. Una con i fake e una con i servizi esterni. Questo per poter lanciare nella nostra CI e CD la suite con i fake, mentre eventualmente manualmente la suite con i servizi veri quando c’è un rilascio in produzione vero e proprio.

Va tuttavia notato che:

  • un’integrazione così importante sarà molto difficile da mantenere vista la collaborazione di molte entità che magari non gestiamo direttamente.
  • se ci appoggiamo a un ambiente di sandbox/playground questo ambiente è facile che sia instabile, abbia un monte ore scaduto il quale diventa inaccessibile o venga proprio chiuso.
  • in ogni caso un test manuale potrebbe evidenziare diversità tra l’app Android/iOS e non conformità difficili da verificare con dei test automatici (es: colori, ordine degli elementi o semplicemente proprietà non verificate come testi o contenuto di pulsanti magari referenziati per id o tag).

Per quanto il testing manuale sia molto difficile da scalare e molto costoso, a mio avviso ha senso farlo sia per i path principali che per eventuali nuove funzionalità di cui si deve verificare la comformità al design e tra l’app Android e un’eventuale controparte iOS.

E come le testi le varie versioni di Feature Flags?

Ne abbiamo già parlato nella diretta dedicata. Molto in breve, conviene testare i principali happy path e le principali interazioni. E’ naturale che con il crescere dei Feature Flags sarà sempre più difficile prevedere tutte le possibili interazioni. Pertanto conviene mantenere le opzioni in un numero molto basso per evitare che il proliferare delle combinazioni possa darci davvero molti scenari non testati e difficili da prevedere.

Conclusioni

Ci sono vari corsi che ti possono insegnare come fare testing/TDD con Android. Uno di questi è quello di Kodeko che è sicuramente ben fatto, anche se si ferma a un’infarinatura di base.

Se ti interessa più il testing e la logica con cui affrontarlo, secondo me dei “must read” sono Test Driven Development. By Example di Kent Beck e Growing Object-Oriented Software, Guided by Tests di Steve Freeman. Il primo, tramite degli esempi, spiega l’approccio corretto con cui affrontare il TDD. Il secondo, invece, fa vedere la logica e l’approccio con cui si affronta un progetto facendosi guidare dai test. Non sono specifici per Android, ma l’approccio è molto simile.

In questo articolo ho voluto concentrarmi non tanto sul “come fare”, che è il tema onnipresente di molte guide, ma il quando usare uno strumento e quando usare l’altro. E se usare qualche strumento in particolare o proprio farne a meno. Magari sul come fare farò qualche video pratico successivamente con qualche app reale. Spero possa esserti stato utile! Se così fosse, sentiti libero di seguirmi su instagram o su Linkedin per non perderti i miei prossimi articoli 🙂