Tutorial de testare Android: Testarea unitară ca un adevărat droid verde
Publicat: 2022-03-11În calitate de dezvoltatori de aplicații cu experiență, pe măsură ce aplicațiile pe care le dezvoltăm se maturizează, avem sentimentul că este momentul să începem testarea. Regulile de afaceri implică adesea că sistemul trebuie să ofere stabilitate în diferite versiuni. De asemenea, în mod ideal dorim să automatizăm procesul de construire și să publicăm automat aplicația. Pentru aceasta, avem nevoie de instrumente de testare Adnroid pentru a garanta că versiunea funcționează conform așteptărilor.
Testele pot oferi un nivel suplimentar de încredere cu privire la lucrurile pe care le construim. Este dificil (dacă nu imposibil) să construiești un produs perfect, fără erori. Prin urmare, scopul nostru va fi să ne îmbunătățim șansele de a reuși pe piață prin înființarea unei suite de teste care va detecta rapid erorile nou introduse în aplicația noastră.
Când vine vorba de Android și de diferitele platforme mobile în general, testarea aplicațiilor poate fi o provocare. Implementarea testelor unitare și respectarea principiilor de dezvoltare bazată pe teste sau similare poate fi adesea neintuitivă, cel puțin. Cu toate acestea, testarea este importantă și nu ar trebui luată de la sine înțeles sau ignorată. David, Kent și Martin au discutat despre beneficiile și capcanele testării într-o serie de conversații între ei compilate într-un articol intitulat „Este TDD mort?”. De asemenea, puteți găsi acolo conversațiile video reale și puteți obține mai multe informații dacă testarea se potrivește procesului dvs. de dezvoltare și în ce măsură le puteți încorpora, începând de acum.
În acest tutorial de testare Android, vă voi ghida prin testarea de unitate și acceptare, regresie pe Android. Ne vom concentra pe abstractizarea unității de teste pe Android, urmată de exemple de testare de acceptare, cu accent pe realizarea procesului cât mai rapid și simplu posibil pentru a scurta ciclurile de feedback dezvoltator-QA.
Ar trebui să-l citesc?
Acest tutorial va explora diferitele posibilități atunci când vine vorba de testarea aplicațiilor Android. Dezvoltatorii sau managerii de proiect care doresc să înțeleagă mai bine posibilitățile actuale de testare ale platformei Android pot decide folosind acest tutorial dacă doresc să adopte oricare dintre abordările menționate în acest articol. Cu toate acestea, aceasta nu este o soluție de argint, deoarece discuția implicată într-un astfel de subiect variază în mod inerent de la produs la produs, împreună cu termenele limită, calitatea codului de bază a codului, nivelul de cuplare a sistemului, preferința dezvoltatorului în proiectarea arhitecturii, durata de viață proiectată a caracteristicii la test, etc.
Gândire în unități: testare Android
În mod ideal, dorim să testăm independent o unitate/componentă logică a unei arhitecturi. În acest fel, putem garanta că componenta noastră funcționează corect pentru setul de intrări pe care îl așteptăm. Dependența poate fi batjocorită, ceea ce ne va permite să scriem teste care se execută rapid. În plus, vom putea simula diferite stări ale sistemului pe baza intrării furnizate testului, acoperind cazuri exotice din proces.
Scopul testării unitare Android este de a izola fiecare parte a programului și de a arăta că părțile individuale sunt corecte. Un test unitar oferă un contract strict, scris, pe care fragmentul de cod trebuie să îl îndeplinească. Drept urmare, oferă mai multe beneficii. — Wikipedia
Roboelectric
Robolectric este un cadru de testare unitară Android care vă permite să rulați teste în interiorul JVM pe stația dvs. de lucru de dezvoltare. Robolectric rescrie clasele Android SDK pe măsură ce sunt încărcate și le face posibil să ruleze pe un JVM obișnuit, rezultând timpi de testare rapidi. În plus, se ocupă de inflația vizualizărilor, încărcarea resurselor și mai multe lucruri care sunt implementate în codul nativ C pe dispozitivele Android, făcând ca necesitatea emulatoarelor și a dispozitivelor fizice să ruleze teste automate.
Mockito
Mockito este un cadru batjocoritor care ne permite să scriem teste curate în java. Simplifică procesul de creare a dublelor de testare (modeluri), care sunt folosite pentru a înlocui dependențele originale ale unei componente/modul utilizate în producție. Un răspuns StackOverflow discută despre diferențele dintre mock-uri și stub-uri în termeni destul de simpli pe care îi puteți citi pentru a afla mai multe.
// 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));
În plus, cu Mockito putem verifica dacă a fost apelată o metodă:
// 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();
Acum, știm că putem specifica perechi acțiune-reacție care definesc ce se întâmplă odată ce executăm o anumită acțiune pe obiectul/componenta batjocorită. Prin urmare, putem bate joc de module întregi ale aplicației noastre și, pentru fiecare caz de testare, facem ca modulul batjocorit să reacționeze într-un mod diferit. Diferitele moduri vor reflecta stările posibile ale componentei testate și ale perechii de componente batjocorite.
Testarea unitară
În această secțiune, vom presupune arhitectura MVP (Model View Presenter). Activitățile și fragmentele sunt vederile, modelele fiind stratul de depozit pentru apelurile către baza de date sau serviciile de la distanță, iar prezentatorul fiind „creierul” care leagă toate acestea împreună implementând o logică specifică pentru a controla vizualizările, modelele și fluxul de date prin intermediul aplicarea.
Componente abstracte
Vederi și modele batjocoritoare
În acest exemplu de testare Android, vom face joc de vizualizări, modele și componente ale depozitului și vom testa unitatea prezentatorului. Acesta este unul dintre cele mai mici teste, care vizează o singură componentă a arhitecturii. În plus, vom folosi metoda stubbing pentru a stabili un lanț de reacții adecvat și testabil:
@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))); } }
Batjocorirea stratului de rețea globală cu MockWebServer
Este adesea convenabil să poți bate joc de nivelul rețelei globale. MockWebServer ne permite să punem în coadă răspunsuri pentru anumite solicitări pe care le executăm în testele noastre. Acest lucru ne oferă șansa de a simula răspunsuri obscure pe care le așteptăm de la server, dar care nu sunt ușor de reprodus. Ne permite să asigurăm o acoperire completă în timp ce scriem puțin cod suplimentar.
Depozitul de cod al MockWebServer oferă un exemplu frumos la care vă puteți referi pentru o mai bună înțelegere a acestei biblioteci.
Test personalizat duble
Vă puteți scrie propriul model sau componenta de repository și o puteți injecta în test furnizând un modul diferit graficului obiect folosind Dagger (http://square.github.io/dagger/). Avem opțiunea de a verifica dacă starea vizualizării a fost actualizată corect pe baza datelor furnizate de componenta modelului batjocorit:
/** 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); } } }
Teste de rulare
Android Studio
Puteți face cu ușurință clic dreapta pe o clasă de testare, o metodă sau un întreg pachet de testare și puteți rula testele din dialogul de opțiuni din IDE.
Terminal
Rularea testelor aplicației Android de pe terminal creează rapoarte pentru clasele testate în folderul „build” al modulului țintă. Mai mult, dacă intenționați să configurați un proces de construire automatizat, veți folosi abordarea terminalului. Cu Gradle, puteți rula toate testele cu aroma de depanare executând următoarele:
gradle testDebug
Accesarea „test” setului sursă din versiunea Android Studio
Versiunea 1.1 a Android Studio și pluginul Android Gradle oferă suport pentru testarea unitară a codului dvs. Puteți afla mai multe citind documentația lor excelentă despre aceasta. Caracteristica este experimentală, dar și o includere excelentă, deoarece acum puteți comuta cu ușurință între testele unitare și seturile de surse de testare a instrumentelor din IDE. Se comportă în același mod ca și cum ați schimba aromele în IDE.
Ușurarea procesului
Scrierea de teste pentru aplicația Android poate să nu fie la fel de distractivă ca dezvoltarea aplicației originale. Prin urmare, câteva sfaturi despre cum să ușurați procesul de scriere a testelor și să evitați problemele comune în timpul instalării proiectului vă vor ajuta foarte mult.
AssertJ Android
AssertJ Android, după cum probabil ați ghicit din nume, este un set de funcții de ajutor care este construit având în vedere Android. Este o extensie a popularei biblioteci AssertJ. Funcționalitatea oferită de AssertJ Android variază de la afirmații simple, cum ar fi „assertThat(view).isGone()”, la lucruri atât de complexe precum:

assertThat(layout).isVisible() .isVertical() .hasChildCount(4) .hasShowDividers(SHOW_DIVIDERS_MIDDLE)
Cu AssertJ Android și extensibilitatea sa, aveți garantat un punct de plecare simplu și bun pentru scrierea de teste pentru aplicațiile Android.
Calea Roboelectrică și Manifestă
În timp ce utilizați Robolectric, este posibil să observați că trebuie să specificați locația manifestului și că versiunea SDK este setată la 18. Puteți face acest lucru incluzând o adnotare „Config”.
@Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18)
Executarea testelor care necesită Robolectric de la terminal poate introduce noi provocări. De exemplu, este posibil să vedeți excepții precum „Tema nu este setată”. Dacă testele se execută corect din IDE, dar nu din terminal, este posibil să încercați să le rulați dintr-o cale din terminal unde calea manifest specificată nu poate fi rezolvată. Este posibil ca valoarea de configurare codificată hard pentru calea manifestului să nu indice locația corectă din punctul de execuție a comenzii. Acest lucru poate fi rezolvat prin utilizarea de alergători personalizați:
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; } }
Configurare Gradle
Puteți utiliza următoarele pentru a configura Gradle pentru testarea unitară. Poate fi necesar să modificați numele dependențelor și versiunile necesare în funcție de nevoile proiectului.
// 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' }
Roboelectric și servicii de joacă
Dacă utilizați Servicii Google Play, va trebui să vă creați propria constantă întreagă pentru versiunea Serviciilor Play, pentru ca Robolectric să funcționeze corect în această configurație a aplicației.
<meta-data android:name="com.google.android.gms.version" android:value="@integer/gms_version" tools:replace="android:value" />
Dependențe Roboelectrice pentru a sprijini bibliotecile
O altă problemă interesantă de testare este că Robolectric nu poate face referire corect la biblioteci de suport. Soluția este să adăugați un fișier „project.properties” la modulul în care sunt testele. De exemplu, pentru bibliotecile Support-v4 și AppCompat fișierul ar trebui să conțină:
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
Testarea de acceptare/regresie
Testarea de acceptare/regresie automatizează o parte a pasului final de testare într-un mediu Android real, 100% la sută. Nu folosim clase de sistem de operare Android batjocorit la acest nivel - testele sunt rulate pe dispozitive și emulatori reale.
Aceste circumstanțe fac procesul mult mai instabil datorită varietății de dispozitive fizice, configurații de emulator, stări ale dispozitivului și seturi de caracteristici ale fiecărui dispozitiv. În plus, este foarte dependent de versiunea sistemului de operare și de dimensiunea ecranului telefonului pentru a decide cum va fi afișat conținutul.
Este puțin complex să creezi testul potrivit care trece pe o gamă largă de dispozitive, dar, ca întotdeauna, ar trebui să visezi mare, dar să începi cu mic. Crearea de teste cu Robotium este un proces iterativ. Cu câteva trucuri, se poate simplifica foarte mult.
Robotium
Robotium este un cadru de automatizare a testelor Android open source care există din ianuarie 2010. Merită menționat că Robotium este o soluție plătită, dar vine cu o încercare gratuită echitabilă.
Pentru a accelera procesul de scriere a testelor Robotium, vom trece de la scrierea manuală a testelor la înregistrarea de test. Compromisul este între calitatea codului și viteză. Dacă faceți modificări importante în interfața dvs. de utilizator, veți beneficia foarte mult de abordarea înregistrării testelor și de a putea înregistra noi teste rapid.
Testdroid Recorder este un înregistrator de testare gratuit care creează teste Robotium pe măsură ce înregistrează clicurile pe care le efectuați pe interfața cu utilizatorul. Instalarea instrumentului este foarte ușoară, așa cum este descris în documentația lor, însoțită de un videoclip pas cu pas.
Deoarece Testdroid Recorder este un plugin Eclipse și ne referim la Android Studio în acest articol, în mod ideal ar fi un motiv de îngrijorare. Cu toate acestea, în acest caz nu este o problemă, deoarece puteți utiliza pluginul direct cu un APK și înregistrați testele împotriva acestuia.
Odată ce ați creat testele, le puteți copia și lipi în Android Studio, împreună cu orice dependență pe care o necesită recorderul Testdroid și sunteți gata să plecați. Testul înregistrat ar arăta cam ca clasa de mai jos:
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; } } }
Dacă te uiți cu atenție, vei observa cât de mult din cod este destul de simplu.
Când înregistrați teste, nu ratați declarațiile „așteptați”. Așteptați să apară dialoguri, să apară activități, să apară texte. Acest lucru va garanta că activitatea și ierarhia de vizualizare sunt gata pentru a fi interacționate atunci când efectuați acțiunea pe ecranul curent. În același timp, faceți capturi de ecran. Testele automate sunt de obicei nesupravegheate, iar capturile de ecran sunt una dintre modalitățile prin care puteți vedea ce s-a întâmplat de fapt în timpul acelor teste.
Indiferent dacă testele trec sau eșuează, rapoartele sunt cel mai bun prieten al tău. Le puteți găsi în directorul de compilare „module/build/outputs/reports”:
În teorie, echipa QA ar putea înregistra teste și să le optimizeze. Depunând efort într-un model standardizat pentru optimizarea cazurilor de testare, s-ar putea realiza. Când înregistrați în mod normal teste, trebuie întotdeauna să modificați câteva lucruri pentru a le face să funcționeze impecabil.
În cele din urmă, pentru a rula aceste teste din Android Studio, le puteți selecta și rula așa cum ați rula teste unitare. Din terminal, este o singură linie:
gradle connectedAndroidTest
Performanța testării
Testarea unității Android cu Robolectric este extrem de rapidă, deoarece rulează direct în JVM-ul de pe computer. În comparație cu asta, testarea de acceptare pe emulatoare și dispozitive fizice este mult mai lentă. În funcție de dimensiunea fluxurilor pe care le testați, poate dura de la câteva secunde la câteva minute pentru fiecare caz de testare. Faza de testare de acceptare ar trebui utilizată ca parte a unui proces de construire automatizat pe un server de integrare continuă.
Viteza poate fi îmbunătățită prin paralelizare pe mai multe dispozitive. Consultați acest instrument grozav de la Jake Wharton și băieții de la Square http://square.github.io/spoon/. Are și niște reportaje frumoase.
The Takeaway
Există o varietate de instrumente de testare Android disponibile și, pe măsură ce ecosistemul se maturizează, procesul de configurare a unui mediu testabil și de scriere a testelor va deveni mai ușor. Mai sunt încă mai multe provocări de abordat, iar cu o comunitate largă de dezvoltatori care lucrează la problemele zilnice, există mult loc pentru discuții constructive și feedback rapid.
Folosiți abordările descrise în acest tutorial de testare Android pentru a vă ghida în abordarea provocărilor care vă așteaptă. Dacă și când întâmpinați probleme, verificați din nou acest articol sau referințele legate de acestea pentru a găsi soluții la problemele cunoscute.
Într-o postare viitoare, vom discuta despre paralelizare, automatizarea construcției, integrarea continuă, cârlige Github/BitBucket, versiunea artefactelor și cele mai bune practici pentru gestionarea proiectelor masive de aplicații mobile în profunzime.