Tutorial di test Android: test unitari come un vero droide verde

Pubblicato: 2022-03-11

In qualità di sviluppatori di app esperti, man mano che le applicazioni che sviluppiamo maturano, abbiamo la sensazione che sia giunto il momento di iniziare a testare. Le regole aziendali spesso implicano che il sistema deve fornire stabilità durante le diverse versioni. Idealmente, desideriamo anche automatizzare il processo di compilazione e pubblicare automaticamente l'applicazione. Per questo, abbiamo bisogno di strumenti di test Adnroid in atto per garantire che la build funzioni come previsto.

I test possono fornire un ulteriore livello di sicurezza sulle cose che costruiamo. È difficile (se non impossibile) costruire un prodotto perfetto e privo di bug. Pertanto, il nostro obiettivo sarà quello di migliorare le nostre probabilità di avere successo sul mercato creando una suite di test che individuerà rapidamente i bug introdotti di recente nella nostra applicazione.

Tutorial test Android

Quando si tratta di Android e delle varie piattaforme mobili in generale, il test delle app può essere una sfida. L'implementazione dei test unitari e il rispetto dei principi dello sviluppo basato su test o simili possono spesso sembrare perlomeno poco intuitivi. Tuttavia, il test è importante e non dovrebbe essere dato per scontato o ignorato. David, Kent e Martin hanno discusso i vantaggi e le insidie ​​dei test in una serie di conversazioni tra loro raccolte in un articolo intitolato "TDD è morto?". Puoi anche trovare le conversazioni video effettive lì e ottenere maggiori informazioni se i test si adattano al tuo processo di sviluppo e in che misura potresti incorporarli, a partire da ora.

In questo tutorial sui test di Android ti guiderò attraverso l'unità e l'accettazione, i test di regressione su Android. Ci concentreremo sull'astrazione dell'unità di test su Android, seguita da esempi di test di accettazione, con l'obiettivo di rendere il processo il più veloce e semplice possibile per abbreviare i cicli di feedback degli sviluppatori-QA.

Dovrei leggerlo?

Questo tutorial esplorerà le diverse possibilità quando si tratta di testare le applicazioni Android. Gli sviluppatori o i project manager che vogliono comprendere meglio le attuali possibilità di test della piattaforma Android possono decidere di utilizzare questo tutorial se vogliono adottare uno degli approcci menzionati in questo articolo. Tuttavia, questo non è un proiettile d'argento, poiché la discussione coinvolta in un argomento del genere varia intrinsecamente da prodotto a prodotto insieme a scadenze, qualità del codice della base di codice, livello di accoppiamento del sistema, preferenza dello sviluppatore nella progettazione dell'architettura, durata prevista della funzionalità per prova, ecc.

Pensare in unità: test Android

Idealmente, vogliamo testare un'unità/componente logica di un'architettura in modo indipendente. In questo modo possiamo garantire che il nostro componente funzioni correttamente per l'insieme di input che ci aspettiamo. Le dipendenze possono essere derise, il che ci consentirà di scrivere test che vengono eseguiti velocemente. Inoltre, saremo in grado di simulare diversi stati del sistema in base all'input fornito al test, coprendo casi esotici nel processo.

L'obiettivo del test unitario di Android è isolare ogni parte del programma e mostrare che le singole parti sono corrette. Uno unit test fornisce un contratto scritto rigoroso che il pezzo di codice deve soddisfare. Di conseguenza, offre diversi vantaggi. —Wikipedia

Robotico

Robolectric è un framework di unit test Android che ti consente di eseguire test all'interno della JVM sulla tua workstation di sviluppo. Robolectric riscrive le classi dell'SDK Android durante il caricamento e ne consente l'esecuzione su una normale JVM, con tempi di test rapidi. Inoltre, gestisce l'inflazione delle visualizzazioni, il caricamento delle risorse e altro ancora implementato nel codice C nativo sui dispositivi Android, rendendo obsoleta la necessità di emulatori e dispositivi fisici per eseguire test automatizzati.

Mockito

Mockito è un framework beffardo che ci consente di scrivere test puliti in java. Semplifica il processo di creazione dei test double (mock), che vengono utilizzati per sostituire le dipendenze originali di un componente/modulo utilizzato nella produzione. Una risposta StackOverflow discute le differenze tra mock e stub in termini abbastanza semplici che puoi leggere per saperne di più.

 // you can mock concrete classes, not only interfaces LinkedList mockedList = mock(LinkedList.class); // stubbing appears before the actual execution when(mockedList.get(0)).thenReturn("first"); // the following prints "first" System.out.println(mockedList.get(0)); // the following prints "null" because get(999) was not stubbed System.out.println(mockedList.get(999));

Inoltre, con Mockito possiamo verificare se è stato chiamato un metodo:

 // mock creation List mockedList = mock(List.class); // using mock object - it does not throw any "unexpected interaction" exception mockedList.add("one"); mockedList.clear(); // selective, explicit, highly readable verification verify(mockedList).add("one"); verify(mockedList).clear(); 

Testdroide

Ora, sappiamo che possiamo specificare coppie azione-reazione che definiscono cosa accade una volta eseguita un'azione specifica sull'oggetto/componente deriso. Pertanto, possiamo deridere interi moduli della nostra applicazione e per ogni test case fare in modo che il modulo deriso reagisca in un modo diverso. I diversi modi rifletteranno i possibili stati del componente testato e della coppia di componenti derisi.

Test unitario

In questa sezione, assumeremo l'architettura MVP (Model View Presenter). Le attività e i frammenti sono le viste, i modelli sono il livello del repository per le chiamate al database o ai servizi remoti e il presentatore è il "cervello" che lega tutti questi insieme implementando una logica specifica per controllare le viste, i modelli e il flusso di dati attraverso il applicazione.

Componenti di astrazione

Viste e modelli beffardi

In questo esempio di test di Android, prenderemo in giro viste, modelli e componenti del repository e testeremo il presentatore. Questo è uno dei test più piccoli, mirato a un singolo componente nell'architettura. Inoltre, utilizzeremo il metodo stub per impostare una catena di reazioni corretta e verificabile:

 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterTest { private Calendar cal = Calendar.getInstance(); @Mock private IFitnessListModel model; @Mock private IFitnessListView view; private IFitnessListPresenter presenter; @Before public void setup() { MockitoAnnotations.initMocks(this); final FitnessEntry entryMock = mock(FitnessEntry.class); presenter = new FitnessListPresenter(view, model); /* Define the desired behaviour. Queuing the action in "doAnswer" for "when" is executed. Clear and synchronous way of setting reactions for actions (stubbing). */ doAnswer((new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { ArrayList<FitnessEntry> items = new ArrayList<>(); items.add(entryMock); ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); return null; } })).when(model).fetchAllItems((IFitnessListPresenterCallback) presenter); } /** Verify if model.fetchItems was called once. Verify if view.onFetchSuccess is called once with the specified list of type FitnessEntry The concrete implementation of ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); calls the view.onFetchSuccess(...) method. This is why we verify that view.onFetchSuccess is called once. */ @Test public void testFetchAll() { presenter.fetchAllItems(false); // verify can be called only on mock objects verify(model, times(1)).fetchAllItems((IFitnessListPresenterCallback) presenter); verify(view, times(1)).onFetchSuccess(new ArrayList<>(anyListOf(FitnessEntry.class))); } }

Prendere in giro il livello di rete globale con MockWebServer

Spesso è conveniente essere in grado di deridere il livello di rete globale. MockWebServer ci consente di accodare le risposte per richieste specifiche che eseguiamo nei nostri test. Questo ci dà la possibilità di simulare risposte oscure che ci aspettiamo dal server, ma non sono semplici da riprodurre. Ci consente di garantire una copertura completa mentre scriviamo poco codice aggiuntivo.

Il repository di codice di MockWebServer fornisce un chiaro esempio a cui puoi fare riferimento per una migliore comprensione di questa libreria.

Doppio test personalizzato

Puoi scrivere il tuo modello o componente di repository e inserirlo nel test fornendo un modulo diverso al grafico dell'oggetto utilizzando Dagger (http://square.github.io/dagger/). Abbiamo la possibilità di verificare se lo stato di visualizzazione è stato aggiornato correttamente in base ai dati forniti dal componente del modello simulato:

 /** Custom mock model class */ public class FitnessListErrorTestModel extends FitnessListModel { // ... @Override public void fetchAllItems(IFitnessListPresenterCallback callback) { callback.onError(); } @Override public void fetchItemsInRange(final IFitnessListPresenterCallback callback, DateFilter filter) { callback.onError(); } }
 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterDaggerTest { private FitnessActivity activity; private FitnessListFragment fitnessListFragment; @Before public void setup() { /* setupActivity runs the Activity lifecycle methods on the specified class */ activity = Robolectric.setupActivity(FitnessActivity.class); fitnessListFragment = activity.getFitnessListFragment(); /* Create the objectGraph with the TestModule */ ObjectGraph localGraph = ObjectGraph.create(TestModule.newInstance(fitnessListFragment)); /* Injection */ localGraph.inject(fitnessListFragment); localGraph.inject(fitnessListFragment.getPresenter()); } @Test public void testInteractorError() { fitnessListFragment.getPresenter().fetchAllItems(false); /* suppose that our view shows a Toast message with the specified text below when an error is reported, so we check for it. */ assertEquals(ShadowToast.getTextOfLatestToast(), "Something went wrong!"); } @Module( injects = { FitnessListFragment.class, FitnessListPresenter.class }, overrides = true, library = true ) static class TestModule { private IFitnessListView view; private TestModule(IFitnessListView view){ this.view = view; } public static TestModule newInstance(IFitnessListView view){ return new TestModule(view); } @Provides public IFitnessListInteractor provideFitnessListInteractor(){ return new FitnessListErrorTestModel(); } @Provides public IFitnessListPresenter provideFitnessPresenter(){ return new FitnessListPresenter(view); } } }

Esecuzione di test

Android Studio

Puoi facilmente fare clic con il pulsante destro del mouse su una classe di test, un metodo o un intero pacchetto di test ed eseguire i test dalla finestra di dialogo delle opzioni nell'IDE.

terminale

L'esecuzione di test dell'app Android dal terminale crea report per le classi testate nella cartella "build" del modulo di destinazione. Inoltre, se prevedi di impostare un processo di compilazione automatizzato, utilizzerai l'approccio del terminale. Con Gradle, puoi eseguire tutti i test aromatizzati di debug eseguendo quanto segue:

 gradle testDebug

Accesso al set di sorgenti "test" dalla versione Android Studio

La versione 1.1 di Android Studio e il plug-in Android Gradle offrono il supporto per il test delle unità del codice. Puoi saperne di più leggendo la loro eccellente documentazione su di esso. La funzionalità è sperimentale, ma è anche un'ottima inclusione poiché ora puoi passare facilmente dai test unitari ai set di sorgenti dei test di strumentazione dall'IDE. Si comporta allo stesso modo come se dovessi cambiare i gusti nell'IDE.

Test dell'unità Android

Facilitare il processo

Scrivere test di app Android potrebbe non essere divertente come sviluppare l'applicazione originale. Quindi, alcuni suggerimenti su come facilitare il processo di scrittura dei test ed evitare problemi comuni durante l'impostazione del progetto saranno di grande aiuto.

AssertJ Android

AssertJ Android, come avrai intuito dal nome, è un insieme di funzioni di supporto creato pensando ad Android. È un'estensione della popolare libreria AssertJ. Le funzionalità fornite da AssertJ Android vanno da semplici asserzioni, come "assertThat(view).isGone()", a cose complesse come:

 assertThat(layout).isVisible() .isVertical() .hasChildCount(4) .hasShowDividers(SHOW_DIVIDERS_MIDDLE)

Con AssertJ Android e la sua estensibilità, ti viene garantito un semplice e buon punto di partenza per scrivere test per applicazioni Android.

Percorso Robotico e Manifesto

Durante l'utilizzo di Robolectric, potresti notare che devi specificare la posizione del manifest e che la versione dell'SDK è impostata su 18. Puoi farlo includendo un'annotazione "Config".

 @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18)

L'esecuzione di test che richiedono Robolectric dal terminale può introdurre nuove sfide. Ad esempio, potresti visualizzare eccezioni come "Tema non impostato". Se i test vengono eseguiti correttamente dall'IDE, ma non dal terminale, è possibile che si stia tentando di eseguirlo da un percorso nel terminale in cui non è possibile risolvere il percorso manifest specificato. Il valore di configurazione hardcoded per il percorso manifest potrebbe non puntare alla posizione corretta dal punto di esecuzione del comando. Questo può essere risolto attraverso l'uso di corridori personalizzati:

 public class RobolectricGradleTestRunner extends RobolectricTestRunner { public RobolectricGradleTestRunner(Class<?> testClass) throws InitializationError { super(testClass); } @Override protected AndroidManifest getAppManifest(Config config) { String appRoot = "../app/src/main/"; String manifestPath = appRoot + "AndroidManifest.xml"; String resDir = appRoot + "res"; String assetsDir = appRoot + "assets"; AndroidManifest manifest = createAppManifest(Fs.fileFromPath(manifestPath), Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)); return manifest; } }

Configurazione della gradazione

È possibile utilizzare quanto segue per configurare Gradle per il test unitario. Potrebbe essere necessario modificare i nomi delle dipendenze e le versioni richieste in base alle esigenze del progetto.

 // Robolectric testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.9.5' testCompile 'com.squareup.dagger:dagger:1.2.2' testProvided 'com.squareup.dagger:dagger-compiler:1.2.2' testCompile 'com.android.support:support-v4:21.0.+' testCompile 'com.android.support:appcompat-v7:21.0.3' testCompile('org.robolectric:robolectric:2.4') { exclude module: 'classworlds' exclude module: 'commons-logging' exclude module: 'httpclient' exclude module: 'maven-artifact' exclude module: 'maven-artifact-manager' exclude module: 'maven-error-diagnostics' exclude module: 'maven-model' exclude module: 'maven-project' exclude module: 'maven-settings' exclude module: 'plexus-container-default' exclude module: 'plexus-interpolation' exclude module: 'plexus-utils' exclude module: 'wagon-file' exclude module: 'wagon-http-lightweight' exclude module: 'wagon-provider-api' }

Servizi di robotica e giochi

Se stai utilizzando Google Play Services, dovrai creare la tua costante intera per la versione di Play Services affinché Robolectric funzioni correttamente in questa configurazione dell'applicazione.

 <meta-data android:name="com.google.android.gms.version" android:value="@integer/gms_version" tools:replace="android:value" />

Dipendenze robotiche a supporto delle biblioteche

Un altro problema di test interessante è che Robolectric non è in grado di fare riferimento alle librerie di supporto correttamente. La soluzione è aggiungere un file "project.properties" al modulo in cui si trovano i test. Ad esempio, per le librerie Support-v4 e AppCompat il file dovrebbe contenere:

 android.library.reference.1=../../build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3 android.library.reference.2=../../build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.3

Test di accettazione/regressione

I test di accettazione/regressione automatizzano parte della fase finale del test su un ambiente Android reale al 100%. Non utilizziamo classi di sistema operativo Android derise a questo livello: i test vengono eseguiti su dispositivi ed emulatori reali.

test di accettazione e regressione di Android

Queste circostanze rendono il processo molto più instabile a causa della varietà di dispositivi fisici, configurazioni dell'emulatore, stati dei dispositivi e set di funzionalità di ciascun dispositivo. Inoltre, la modalità di visualizzazione del contenuto dipende fortemente dalla versione del sistema operativo e dalle dimensioni dello schermo del telefono.

È un po' complesso creare il test giusto che superi un'ampia gamma di dispositivi, ma come sempre dovresti sognare in grande e iniziare in piccolo. La creazione di test con Robotium è un processo iterativo. Con alcuni trucchi, può essere molto semplificato.

Robotio

Robotium è un framework di automazione dei test Android open source che esiste da gennaio 2010. Vale la pena ricordare che Robotium è una soluzione a pagamento, ma viene fornita con una prova gratuita equa.

Per accelerare il processo di scrittura dei test Robotium, passeremo dalla scrittura manuale dei test alla registrazione dei test. Il compromesso è tra qualità del codice e velocità. Se stai apportando modifiche sostanziali alla tua interfaccia utente, trarrai grandi benefici dall'approccio di registrazione dei test e dalla possibilità di registrare nuovi test rapidamente.

Testdroid Recorder è un registratore di test gratuito che crea test Robotium in quanto registra i clic eseguiti sull'interfaccia utente. Installare lo strumento è semplicissimo, come descritto nella loro documentazione accompagnata da un video passo-passo.

Poiché Testdroid Recorder è un plug-in Eclipse e in questo articolo ci riferiamo ad Android Studio, idealmente sarebbe motivo di preoccupazione. Tuttavia, in questo caso non è un problema, in quanto puoi utilizzare il plug-in direttamente con un APK e registrare i test a fronte di esso.

Una volta creati i test, puoi copiarli e incollarli in Android Studio, insieme a qualsiasi dipendenza richiesta dal registratore Testdroid, e sei pronto per partire. Il test registrato sarebbe simile alla classe seguente:

 public class LoginTest extends ActivityInstrumentationTestCase2<Activity> { private static final String LAUNCHER_ACTIVITY_CLASSNAME = "com.toptal.fitnesstracker.view.activity.SplashActivity"; private static Class<?> launchActivityClass; static { try { launchActivityClass = Class.forName(LAUNCHER_ACTIVITY_CLASSNAME); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } private ExtSolo solo; @SuppressWarnings("unchecked") public LoginTest() { super((Class<Activity>) launchActivityClass); } // executed before every test method @Override public void setUp() throws Exception { super.setUp(); solo = new ExtSolo(getInstrumentation(), getActivity(), this.getClass() .getCanonicalName(), getName()); } // executed after every test method @Override public void tearDown() throws Exception { solo.finishOpenedActivities(); solo.tearDown(); super.tearDown(); } public void testRecorded() throws Exception { try { assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_username_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_username_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_username_input"), "[email protected]"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_password_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_password_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_password_input"), "123456"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); solo.clickOnButton((Button) solo .findViewById("com.toptal.fitnesstracker.R.id.parse_login_button")); assertTrue("Wait for text fitness list activity.", solo.waitForActivity(FitnessActivity.class)); assertTrue("Wait for text KM.", solo.waitForText("KM", 20000)); /* Custom class that enables proper clicking of ActionBar action items */ TestUtils.customClickOnView(solo, R.id.action_logout); solo.waitForDialogToOpen(); solo.waitForText("OK"); solo.clickOnText("OK"); assertTrue("waiting for ParseLoginActivity after logout", solo.waitForActivity(ParseLoginActivity.class)); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); } catch (AssertionFailedError e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } catch (Exception e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } } }

Se guardi da vicino, noterai quanto del codice sia piuttosto semplice.

Quando si registrano i test, non scarseggiare con le dichiarazioni di "attesa". Attendi che appaiano finestre di dialogo, attività, testi. Ciò garantirà che l'attività e la gerarchia di visualizzazione siano pronte per essere utilizzate quando si esegue l'azione nella schermata corrente. Allo stesso tempo, fai screenshot. I test automatici di solito non sono presidiati e gli screenshot sono uno dei modi in cui puoi vedere cosa è effettivamente successo durante quei test.

Indipendentemente dal fatto che i test vengano superati o meno, i rapporti sono i tuoi migliori amici. Puoi trovarli nella directory build "module/build/outputs/reports":

rapporto di prova

In teoria, il team QA potrebbe registrare i test e ottimizzarli. Impegnandosi in un modello standardizzato per l'ottimizzazione dei casi di test, è possibile. Quando normalmente registri i test, devi sempre modificare un paio di cose per farlo funzionare perfettamente.

Infine, per eseguire questi test da Android Studio, puoi selezionarli ed eseguirli come faresti con gli unit test. Dal terminale, è un one-liner:

 gradle connectedAndroidTest

Esecuzione del test

Il test delle unità Android con Robolectric è estremamente veloce, perché viene eseguito direttamente all'interno della JVM sul tuo computer. Rispetto a ciò, i test di accettazione su emulatori e dispositivi fisici sono molto più lenti. A seconda delle dimensioni dei flussi che stai testando, possono essere necessari da pochi secondi a pochi minuti per ogni test case. La fase del test di accettazione deve essere utilizzata come parte di un processo di compilazione automatizzato su un server di integrazione continua.

La velocità può essere migliorata mediante la parallelizzazione su più dispositivi. Dai un'occhiata a questo fantastico strumento di Jake Wharton e dei ragazzi di Square http://square.github.io/spoon/. Ha anche dei bei reportage.

L'asporto

Sono disponibili una varietà di strumenti di test Android e, man mano che l'ecosistema matura, il processo di creazione di un ambiente testabile e la scrittura di test diventeranno più semplici. Ci sono ancora più sfide da affrontare e, con un'ampia comunità di sviluppatori che lavora su problemi quotidiani, c'è molto spazio per discussioni costruttive e feedback veloci.

Usa gli approcci descritti in questo tutorial di test Android per guidarti nell'affrontare le sfide che ti attendono. Se e quando si verificano problemi, ricontrolla con questo articolo o i riferimenti collegati all'interno per soluzioni a problemi noti.

In un prossimo post parleremo di parallelizzazione, build automation, integrazione continua, hook Github/BitBucket, controllo delle versioni degli artefatti e le migliori pratiche per gestire in modo più approfondito grandi progetti di applicazioni mobili.