Oltre a quanto visto nelle lezioni precedenti, anche le interfacce utente possono essere sottoposte a test. Si usano per questo appositi framework che interagiscono con i controlli utente come se a farlo fosse una mano invisibile. Alla fine di ciò, si possono valutare i risultati verificando se lo stato finale dell'applicazione corrisponde alle nostre attese.
Ci sono varie alternative per eseguire test sull'interfaccia utente, ma qui ci concentreremo su Espresso, framework ideato da Google, ormai integrato in un progetto di più ampio respiro denominato Android Testing Support Library. Quest'ultimo ramo della libreria di supporto possiede, oltre ad Espresso, altri due set di API:
- AndroidJUnitRunner: una classe in grado di eseguire test instrumented su dispositivi Android in stile JUnit3 e JUnit4;
- UI Automator: altro strumento per il collaudo di interfacce utente che permette di effettuare test funzionali cross-app, tra sistema e app installate.
Per utilizzare questo framework, è necessario possedere la Android Support Library, come presumibile, e tenerla aggiornata aggiungendo, nel file build.gradle del modulo applicativo, le seguenti dipendenze:
dependencies {
...
androidTestCompile 'com.android.support:support-annotations:24.2.1';
androidTestCompile 'com.android.support.test:runner:0.5';
androidTestCompile 'com.android.support.test:rules:0.5';
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2';
...
}
Inoltre, nel blocco defaultConfig
, dobbiamo aggiungere questa espressione:
android
{
...
...
defaultConfig
{
...
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
...
}
...
...
}
Si ricordi che i test che andremo a creare saranno test instrumented, ovvero eseguiti su dispositivo (reale o emulato) e compilati in un vero e proprio pacchetto APK.
Applicazione per l'esempio
Nelle lezioni precedenti, abbiamo svolto esperimenti di testing sulla classe DataManager
, che includeva alcune funzionalità dedicate alla gestione di istanze della classe Persona
. Collochiamo ora tale classe al centro di un Adapter, che collaborerà al completamento dell'interfaccia utente dell'applicazione da testare (i cui sorgenti sono allegati a questa lezione). La figura seguente mostra l'Activity, che include una ListView
con alcuni elementi già inseriti in fase di inizializzazione:
La funzionalità che vogliamo sottoporre a test consiste nella cancellazione di un elemento gestito dall'Adapter: inizieremo con un click su un pulsante della riga, e l'operazione verrà portata a termine previa conferma tramite finestra di dialogo.
Funzionamento di Espresso
I test prodotti con Espresso risultano perfettamente fluidi in quanto il framework riesce a sincronizzare le invocazioni ai controlli utente percependo gli stati idle del thread principale e la visibilità degli elementi. Ogni operazione che svolgeremo, grosso modo, sarà distribuita in tre fasi:
- invocheremo il componente visuale sul quale vorremo simulare l'interazione come, ad esempio, un pulsante da premere. Faremo ciò di norma con il metodo
onView()
, mentre useremoonData()
solo per gli elementi integrati in un oggettoAdapterView
; - effettueremo l'azione vera e propria tramite il metodo
perform()
, attivato sul risultato prodotto al punto precedente. Il seguente codice Java, ad esempio, produrrà la pressione del pulsante con idbtn_save
:
onView(withId(R.id.btn_save)).perform(click())
- effettueremo controlli tramite
ViewAssertions
per verificare come gli effetti dell'operazione eseguita si sono ripercossi sull'interfaccia utente.
All'interno di un test, le operazioni di ricerca della View e del metodo perform()
possono essere ripetute più volte fino a completare l'interazione che abbiamo in mente. Per quanto riguarda la struttura, la classe dei test sarà in stile JUnit; infatti, utilizzeremo le annotazioni @Test
, @Before
e @After
come abbiamo visto nelle lezioni precedenti. Per poter avere a disposizione una Activity prima di ogni test, sfrutteremo la classe ActivityTestRule
: tali intefacce verranno istanziate e distrutte, rispettivamente, prima dell'invocazione di ogni metodo contrassegnato da @Before
e dopo ognuno di quelli indicati da @After
:
@RunWith(AndroidJUnit4.class)
@LargeTest
public class UI_Test {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
MainActivity.class);
/*
* inseriamo qui tutti i metodi contrassegnati dalle annotazioni @Test, @Before e @After
*
*/
}
Primi test su un'interfaccia utente
Per svolgere test di questo tipo, sarà innanzitutto necessario avere presente la struttura dell'applicazione, soprattutto per quanto riguarda gli ID con cui vengono contraddistinti i controlli utente ed il codice dei metodi da attivare. Nel nostro caso, ad esempio, sfrutteremo nell'ordine:
- una
ListView
nel layout principale:
riconoscibile dall'id<ListView ... android:id="@+id/listView" ... />
listView
; - un layout che darà forma ad ogni riga della
ListView
, contenuto nel file riga.xml, del quale ci interesserà particolarmente l'ID del pulsante contenuto:
<ImageButton ... ... android:onClick="cancellaPersona" android:src="@android:drawable/ic_delete" android:id="@+id/btn_delete"/>
- il metodo Java, definito nell'Activity, che verrà invocato al click del pulsante e che opererà la cancellazione, a patto che venga data conferma tramite finestra di dialogo:
public void cancellaPersona(final View v) { AlertDialog.Builder alert=new AlertDialog.Builder(this); alert.setMessage("L'elemento verrà cancellato definitivamente. Proseguire?"); alert.setPositiveButton("No", null); alert.setNegativeButton("Sì", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { int pos=listView.getPositionForView(v); adapter.remove(pos); } }); alert.show();}
Nel codice di test del primo metodo che proponiamo, eseguiamo la cancellazione di una riga dell'Activity, rispondendo affermativamente alla domanda posta dalla finestra di dialogo. Eseguiremo la verifica sfruttando le assertion di JUnit ed i Matcher di Hamcrest, come abbiamo imparato nelle lezioni precedenti:
@Test
public void deleteListviewItem() {
// 1. click sul pulsante
onData(anything()).inAdapterView(withId(R.id.listView))
.atPosition(0)
.onChildView(withId(R.id.btn_delete))
.perform(click());
// 2. conteggio iniziale del numero di righe della ListView
int initialCount=((ListView)mActivityRule.getActivity().findViewById(R.id.listView)).getCount();
// 3. click sul pulsante "Sì" della finestra di dialogo
onView(withId(android.R.id.button2)).perform(click());
// 4. conteggio finale del numero di righe della ListView
int finalCount=((ListView)mActivityRule.getActivity().findViewById(R.id.listView)).getCount();
// 5. Il numero di righe è diminuito di uno?
assertThat(finalCount, is(equalTo(initialCount-1)));
}
Il test, una volta eseguito su dispositivo, dovrebbe confermarci che la ListView possiede ora una riga in meno. La verifica può essere effettuata in altri modi, considerando che le righe derivano da cosa l'Adapter gestisce: potremmo, ad esempio, predisporre l'interrogazione di quest'ultimo o rinunciare alle assertion di JUnit utilizzando una riga come la seguente:
onData(anything()).inAdapterView(withId(R.id.listView)).atPosition(3).check(matches(isDisplayed()));
che verifica se la quarta riga della ListView è ancora visualizzata. Ulteriori approfondimenti sull'argomento verranno offerti nelle prossime lezioni.