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:
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.
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.
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:
onView()
, mentre useremo onData()
solo per gli elementi integrati in un oggetto AdapterView
;perform()
, attivato sul risultato prodotto al punto precedente. Il seguente codice Java, ad esempio, produrrà la pressione del pulsante con id btn_save
:
onView(withId(R.id.btn_save)).perform(click())
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 * */ }
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:
ListView
nel layout principale:
<ListView ... android:id="@+id/listView" ... />
riconoscibile dall’id listView
;
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"/>
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.
inserisci la tua e-mail nel box qui sotto: