Testing og Dependency Injection With Model View Presenter på Android

Vi utforsket konseptene i modellvisningsmodellmønsteret i den første delen av denne serien, og vi implementerte vår egen versjon av mønsteret i den andre delen. Nå er det dags å grave litt dypere. I denne opplæringen fokuserer vi på følgende emner:

  • sette opp testmiljøet og skrive enhetstester for MVP-klassene
  • implementere MVP-mønsteret ved hjelp av avhengighetsinjeksjon med Dagger 2
  • Vi diskuterer vanlige problemer å unngå når du bruker MVP på Android

1. Enhetstesting

En av de største fordelene ved å vedta MVP-mønsteret er at det forenkler enhetstesting. Så la oss skrive tester for modell- og presentatorklassene vi opprettet og implementert i den siste delen av denne serien. Vi skal kjøre testene våre ved hjelp av Robolectric, en enhetstestramme som gir mange nyttige stubber for Android-klasser. For å lage mock objekter, vil vi bruke Mockito, som gjør at vi kan verifisere om visse metoder ble kalt.

Trinn 1: Oppsett

Rediger build.gradle fil av appmodulet ditt og legg til følgende avhengigheter.

avhengigheter // ... testCompile 'junit: junit: 4.12' // Angi denne avhengigheten hvis du vil bruke Hamcrest matching testCompile 'org.hamcrest: hamcrest-bibliotek: 1.1' testCompile "org.robolectric: robolectric: 3.0" testCompile 'org .mockito: mockito-kjernen: 1.10.19 '

Inne i prosjektets src mappe, opprett følgende mappestruktur test / java / [pakkenavn] / [app-name]. Deretter oppretter du en feilsøkingskonfigurasjon for å kjøre testpakken. Klikk Rediger konfigurasjoner ...  på toppen.

Klikk på + knappen og velg JUnit fra listen.

Sett Arbeidsregister til $ MODULE_DIR $.

Vi vil at denne konfigurasjonen skal kjøre alle enhetstester. Sett Test slag til Alt i pakken og skriv inn pakkens navn i Pakke felt.

Trinn 2: Testing av modellen

La oss starte testene med modell-klassen. Enhetsprøven kjører med RobolectricGradleTestRunner.class, som gir ressursene som trengs for å teste Android-spesifikke operasjoner. Det er viktig å annotere @Cofing med følgende alternativer:

@RunWith (RobolectricGradleTestRunner.class) // Endre det som er nødvendig for prosjektet ditt @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") offentlig klasse MainModelTest // skriv tester

Vi vil bruke en ekte DAO (data access object) for å teste om dataene håndteres riktig. Å få tilgang til en Kontekst, vi bruker RuntimeEnvironment.application klasse.

privat DAO mDAO; // For å teste modellen kan du bare // opprette objektet og og sende // en Presents mock og en DAO-forekomst @For offentlig tomromoppsett () // Bruke RuntimeEnvironment.application tillater // oss å få tilgang til en Kontekst og lage en ekte DAO // sette inn data som vil bli lagret midlertidig Context context = RuntimeEnvironment.application; mDAO = ny DAO (kontekst); // Ved hjelp av en mock Presenter vil tillate å verifisere // hvis bestemte metoder ble kalt i Presenter MainPresenter mockPresenter = Mockito.mock (MainPresenter.class); // Vi lager en modelleksempel ved hjelp av en konstruksjon som inkluderer // en DAO. Denne konstruktøren eksisterer for å lette tester mModel = ny MainModel (mockPresenter, mDAO); // Abonnere mNotes er nødvendig for testmetoder // som avhenger av arrayList mModel.mNotes = new ArrayList <> (); // Vi reseterer vår mock Presenter for å garantere at // vår metodeverifisering forblir konsistent mellom testen nullstilles (mockPresenter); 

Det er nå på tide å teste modellens metoder.

// Opprett notatobjekt som skal brukes i testene privat Note createNote (String text) Note note = new Note (); note.setText (tekst); note.setDate ("noen dato"); retur notat;  // Verifiser loadData @Test public void loadData () int notesSize = 10; // setter inn data direkte ved hjelp av DAO for (int i = 0; i -1);  // Verifiser deleteNote @Test public void deleteNote () // Vi må legge til en notat i DB Note note = createNote ("testNote"); Merk insertedNote = mDAO.insertNote (notat); // legge til samme notat i mNotes ArrayList mModel.mNotes = new ArrayList <> (); mModel.mNotes.add (insertedNote); // verifisere om deleteNote returnerer de riktige resultatene assertTrue (mModel.deleteNote (insertedNote, 0)); Merk fakeNote = createNote ("fakeNote"); assertFalse (mModeldelteNote (fakeNote, 0)); 

Du kan nå kjøre modelltesten og sjekke resultatene. Du er velkommen til å teste andre aspekter av klassen.

Trinn 3: Testing av presentatøren

La oss nå fokusere på å teste presentatøren. Vi trenger også Robolectric for denne testen for å gjøre bruk av flere Android-klasser, for eksempel AsyncTask. Konfigurasjonen er veldig lik modelltesten. Vi bruker View og Model mocks for å verifisere metallsamtaler og definere returverdier.

@RunWith (RobolectricGradleTestRunner.class) @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") offentlig klasse MainPresenterTest privat MainPresenter mPresenter; private MainModel mockModel; privat MVP_Main.RequiredViewOps mockView; // For å teste presentatøren kan du bare // opprette objektet og sende modell- og visningsmocksene @For offentlig tomromoppsett () // Opprette mocks mockView = Mockito.mock (MVP_Main.RequiredViewOps.class); mockModel = Mockito.mock (MainModel.class, RETURNS_DEEP_STUBS); // Pass mocks til en Presenter-forekomst mPresenter = ny MainPresenter (mockView); mPresenter.setModel (mockModel); // Definer verdien som skal returneres av Model // når du laster inn data når (mockModel.loadData ()). ThenReturn (true); reset (mockView); 

For å teste presentatørens metoder, la oss begynne med clickNewNote () operasjon, som er ansvarlig for å lage et nytt notat og registrere det i databasen ved hjelp av en AsyncTask.

@Test public void testClickNewNote () // Vi må spotte en EditText EditText mockEditText = Mockito.mock (EditText.class, RETURNS_DEEP_STUBS); // mock bør returnere en streng når (mockEditText.getText () .String ()). thenReturn ("Test_true"); // vi definerer også en falsk posisjon som skal returneres // ved insert note-metoden i Model int arrayPos = 10, når (mockModel.insertNote (any (Note.class))). ThenReturn (arrayPos); mPresenter.clickNewNote (mockEditText); verifiser (mockModel) .insertNote (noen (Note.class)); bekreft (mockView) .notifyItemInserted (eq (arrayPos + 1)); bekreft (mockView) .notifyItemRangeChanged (eq (arrayPos), anyInt ()); bekreft (mockView, never ()). showToast (any (Toast.class));

Vi kan også teste et scenario der insertNote () metode returnerer en feil.

@Test public void testClickNewNoteError () EditText mockEditText = Mockito.mock (EditText.class, RETURNS_DEEP_STUBS); når (mockModel.insertNote (en hvilken som helst (Note.class))) thenReturn (-1).; når (mockEditText.getText () toString ().) thenReturn ( "Test_false."); når (mockModel.insertNote (en hvilken som helst (Note.class))) thenReturn (-1).; mPresenter.clickNewNote (mockEditText); verifisere (mockView) .showToast (en hvilken som helst (Toast.class)); 

Til slutt tester vi deleteNote () metode, vurderer både et vellykket og et mislykket resultat.

@Test public void testDeleteNote () når (mockModel.deleteNote (any (Note.class), anyInt ())). ThenReturn (true); int adapterPos = 0; int layoutPos = 1; mPresenter.deleteNote (ny Note (), adapterPos, layoutPos); verifisere (mockView) .showProgress (); verifiser (mockModel) .deleteNote (noen (Note.class), eq (adapterPos)); verifisere (mockView) .hideProgress (); verifisere (mockView) .notifyItemRemoved (likning (layoutPos)); verifisere (mockView) .showToast (en hvilken som helst (Toast.class));  @Test public void testDeleteNoteError () når (mockModel.deleteNote (any (Note.class), anyInt ())). ThenReturn (false); int adapterPos = 0; int layoutPos = 1; mPresenter.deleteNote (ny Note (), adapterPos, layoutPos); verifisere (mockView) .showProgress (); verifiser (mockModel) .deleteNote (noen (Note.class), eq (adapterPos)); verifisere (mockView) .hideProgress (); verifisere (mockView) .showToast (en hvilken som helst (Toast.class)); 

2. Avhengighetsinjeksjon med Dagger 2

Dependency Injection er et flott verktøy tilgjengelig for utviklere. Hvis du ikke er kjent med avhengighetsinjeksjon, anbefaler jeg sterkt at du leser Kerrys artikkel om emnet.

Dependensinjeksjon er en stil med objektkonfigurasjon der et objekts felter og samarbeidspartnere er angitt av en ekstern enhet. Med andre ord er objekter konfigurert av en ekstern enhet. Dependensinjeksjon er et alternativ til at objektet konfigurerer seg selv. - Jakob Jenkov

I dette eksemplet tillater avhengighetsinjeksjon at modellen og presentatoren er opprettet utenfor visningen, noe som gjør MVP-lagene løst koblet og øker separasjonen av bekymringer.

Vi bruker Dagger 2, et fantastisk bibliotek fra Google, for å hjelpe oss med avhengighetsinjeksjon. Mens oppsettet er greit, har dagger 2 mange flotte alternativer, og det er et relativt komplekst bibliotek.

Vi konsentrerer oss bare om de relevante delene av biblioteket for å implementere MVP og vil ikke dekke biblioteket i detalj. Hvis du vil lære mer om Dagger, kan du lese Kerrys veiledning eller dokumentasjonen fra Google.

Trinn 1: Konfigurere Dagger 2

Start med å oppdatere prosjektets build.gradle fil ved å legge til en avhengighet.

avhengigheter // ... classpath 'com.neenbedankt.gradle.plugins: android-apt: 1,8'

Deretter redigerer du prosjektets build.dagger filen som vist nedenfor.

bruk plugin: 'com.neenbedankt.android-apt' avhengigheter // apt kommando kommer fra android-apt plugin apt 'com.google.dagger: dagger-compiler: 2.0.2' compile 'com.google.dagger: dagger : 2.0.2 'gitt' org.glassfish: javax.annotation: 10.0-b28 '// ...

Synkroniser prosjektet og vent på at operasjonen skal fullføres.

Trinn 2: Implementere MVP med Dagger 2

La oss begynne med å lage en @Omfang for Aktivitet klasser. Lage en @annotation med navnet på innholdet.

@Scope public @interface ActivityScope 

Deretter jobber vi på en @Module for Hoved aktivitet. Hvis du har flere aktiviteter, bør du gi en @Module for hver Aktivitet.

@Module offentlige klasse MainActivityModule private MainActivity aktivitet; offentlig MainActivityModule (MainActivity Activity) this.activity = aktivitet;  @Provides @ActivityScope MainActivity providesMainActivity () returaktivitet;  @Provides @ActivityScope MVP_Main.ProvidedPresenterOps providedPresenterOps () MainPresenter presenterer = ny MainPresenter (aktivitet); MainModel modell = ny MainModel (presentator); presenterer.setModel (modell); returleverandør; 

Vi trenger også en @Subcomponent å lage en bro med vår søknad @Komponent, som vi fortsatt trenger å lage.

@ActivityScope @Subcomponent (modules = MainActivityModule.class) offentlig grensesnitt MainActivityComponent MainActivity injector (MainActivity aktivitet); 

Vi må lage en @Module og a @Komponent for applikasjon.

@Module offentlig klasse AppModule privat Application application; offentlig AppModule (Application Application) this.application = application;  @Provides @Singleton offentlig søknad girApplication () return application; 
@Singleton @Component (modules = AppModule.class) offentlige grensesnitt AppComponent Application application (); MainActivityComponent getMainComponent (MainActivityModule modul); 

Til slutt trenger vi en applikasjon klasse for å initialisere avhengighetsinjeksjonen.

offentlig klasse SampleApp utvider søknad offentlig statisk SampleApp get (Kontekst kontekst) return (SampleApp) context.getApplicationContext ();  @Override public void onCreate () super.onCreate (); initAppComponent ();  Privat AppComponent appComponent; privat tomt initAppComponent () appComponent = DaggerAppComponent.builder () .appModule (nytt AppModule (dette)) .build ();  offentlig AppComponent getAppComponent () return appComponent; 

Ikke glem å ta med klassenavnet i prosjektets manifest.

Trinn 3: Injiserer MVP-klasser

Til slutt kan vi @Inject våre MVP klasser. Endringene vi må gjøre er gjort i Hoved aktivitet klasse. Vi endrer måten modell og presentator er initialisert på. Det første trinnet er å endre MVP_Main.ProvidedPresenterOps variabel deklarasjon. Det må være offentlig og vi må legge til en @Inject merknad.

@Inject offentlig MVP_Main.ProvidedPresenterOps mPresenter;

For å sette opp MainActivityComponent, legg til følgende:

/ ** * Konfigurer @link com.tinmegali.tutsmvp_sample.di.component.MainActivityComponent * for å instantiere og injisere en @link MainPresenter * / privat void setupComponent () Log.d (TAG, "setupComponent") ; SampleApp.get (this) .getAppComponent () .getMainComponent (new MainActivityModule (this)) .inject (this); 

Alt vi trenger å gjøre nå, er å initialisere eller på nytt oppgi presentatoren, avhengig av tilstanden på StateMaintainer. Endre setupMVP () metode og legg til følgende:

/ ** * Oppsettmodellvisning Presentasjonsmønster. * Bruk en @link StateMaintainer for å vedlikeholde * Presenter og Model-forekomster mellom konfigurasjonsendringer. * / private void setupMVP () if (mStateMaintainer.firstTimeIn ()) initialiser ();  ellers reinitialize ();  / ** * Oppsett @link MainPresenter injeksjonen og lagrer inn mStateMaintainer * / privat tomrom initialiserer () Log.d (TAG, "initialiser"); setupComponent (); mStateMaintainer.put (MainPresenter.class.getSimpleName (), mPresenter);  / ** * Gjenopprett @link MainPresenter fra mStateMaintainer eller oppretter * en ny @link MainPresenter hvis forekomsten har gått tapt fra mStateMaintainer * / privat ugyldig gjenopprettelse () Log.d (TAG, "reinitialize"); mPresenter = mStateMaintainer.get (MainPresenter.class.getSimpleName ()); mPresenter.setView (this); hvis (mPresenter == null) setupComponent (); 

MVP-elementene blir nå konfigurert uavhengig av visningen. Koden er mer organisert takket være bruk av avhengighetsinjeksjon. Du kan forbedre koden din enda mer ved hjelp av avhengighetsinjeksjon for å injisere andre klasser, for eksempel DAO.

3. Unngå vanlige problemer

Jeg har listet opp en rekke vanlige problemer du bør unngå når du bruker modellvisningsmønsteret.

  • Sjekk alltid om visningen er tilgjengelig før du ringer den. Visningen er knyttet til programmets livssyklus og kan ødelegges når forespørselen din blir gitt.
  • Ikke glem å sende en ny referanse fra Vis når den gjenopprettes.
  • Anrop onDestroy () i presentatøren hver gang visningen er ødelagt. I noen tilfeller kan det være nødvendig å informere presentatøren om en onStop eller en onPause begivenhet.
  • Vurder å bruke flere presentere når du arbeider med komplekse visninger.
  • Når du bruker flere presentatører, er den enkleste måten å sende informasjon mellom dem, ved å vedta en slags arrangementsbuss.
  • For å opprettholde visningslaget ditt så passivt som det kan være, bør du vurdere å bruke avhengighetsinjeksjon for å lage presentasjons- og modelllagene utenfor visningen.

Konklusjon

Du nådde slutten av denne serien der vi utforsket modellvisningsmønsteret. Du bør nå kunne implementere MVP-mønsteret i dine egne prosjekter, teste det, og til og med vedta avhengighetsinjeksjon. Jeg håper du har hatt denne reisen så mye som jeg gjorde. Jeg håper å se deg snart.