Testing och Dependency Injection With Model View Presenter på Android

Vi undersökte koncepten för modellvisningspresentmönstret i den första delen av denna serie och vi implementerade vår egen version av mönstret i andra delen. Nu är det dags att gräva lite djupare. I denna handledning fokuserar vi på följande ämnen:

  • inrätta testmiljön och skriva enhetsprov för MVP-klasserna
  • implementering av MVP-mönstret med hjälp av beroendeinsprutning med Dagger 2
  • vi diskuterar vanliga problem att undvika när du använder MVP på Android

1. Enhetstestning

En av de största fördelarna med att anta MVP-mönstret är att det förenklar enhetstestning. Så låt oss skriva tester för modell- och presenterklasserna som vi skapade och genomförde i den sista delen av denna serie. Vi kommer att köra våra tester med Robolectric, en enhetstestram som ger många användbara stubbar för Android-klasser. För att skapa mocka objekt använder vi Mockito, vilket gör att vi kan verifiera om vissa metoder kallades.

Steg 1: Inställning

Redigera build.gradle filen i din appmodul och lägg till följande beroenden.

beroenden // ... testCompile 'junit: junit: 4.12' // Ange detta beroende om du vill använda Hamcrest matchning testCompile 'org.hamcrest: hamcrest-bibliotek: 1.1 "testCompile" org.robolectric: robolectric: 3.0 "testCompile' org .mockito: mockito-core: 1.10.19 '

Inuti projektets src mapp, skapa följande mappstruktur test / java / [paketnamn] / [app-namn]. Skapa sedan en felsöknings konfiguration för att köra testpaketet. Klick Redigera konfigurationer ...  på toppen.

Klicka på + knappen och välj JUnit från listan.

Uppsättning Arbetskatalog till $ MODULE_DIR $.

Vi vill att denna konfiguration ska köra alla enhetstester. Uppsättning Test slag till Allt i paketet och ange paketnamnet i Paket fält.

Steg 2: Testa modellen

Låt oss börja våra tester med modellklassen. Enhetstestet körs med RobolectricGradleTestRunner.class, vilket ger de resurser som behövs för att testa Android specifika operationer. Det är viktigt att kommentera @Cofing med följande alternativ:

@RunWith (RobolectricGradleTestRunner.class) // Ändra vad som är nödvändigt för ditt projekt @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") allmän klass MainModelTest // skriv test

Vi vill använda en riktig DAO (data access object) för att testa om data hanteras korrekt. För att komma åt en Sammanhang, vi använder RuntimeEnvironment.application klass.

privat DAO mDAO; // För att testa modellen kan du bara // skapa objektet och och skicka // en Presents mock och en DAO-instans @ Före offentlig tomrumsinställning () // Användning av RuntimeEnvironment.application tillåter // oss att komma åt en kontext och skapa en riktig DAO // infoga data som kommer att sparas tillfälligt Context context = RuntimeEnvironment.application; mDAO = ny DAO (kontext); // Använda en mock Presenter tillåter att verifiera // om vissa metoder kallades i Presenter MainPresenter mockPresenter = Mockito.mock (MainPresenter.class); // Vi skapar en modellinstans med en konstruktion som innehåller // en DAO. Denna konstruktör finns för att underlätta testen mModel = ny MainModel (mockPresenter, mDAO); // Prenumerera mNotes är nödvändiga för testmetoder // som beror på arrayList mModel.mNotes = new ArrayList <> (); // Vi reseterar vår mock Presenter för att garantera att // vår metodverifiering förblir konsekvent mellan teståterställningen (mockPresenter); 

Nu är det dags att testa modellens metoder.

// Skapa objektsobjekt som ska användas i testerna privata Notera notera (Stringtext) Obs! = Ny anteckning (); note.setText (text); note.setDate ("some date"); returnera not  // Verifiera loadData @Test public void loadData () int notesSize = 10; // infoga data direkt med DAO för (int i = 0; i -1);  // Verifiera deleteNote @Test public void deleteNote () // Vi måste lägga till en anteckning i DB Not note = createNote ("testNote"); Not insertedNote = mDAO.insertNote (notera); // lägg till samma anteckning inom mNotes ArrayList mModel.mNotes = new ArrayList <> (); mModel.mNotes.add (insertedNote); // verifiera om deleteNote returnerar de rätta resultaten assertTrue (mModel.deleteNote (insertedNote, 0)); Notera fakeNote = createNote ("fakeNote"); assertFalse (mModeldeleteNote (fakeNote, 0)); 

Nu kan du köra modelltestet och kontrollera resultaten. Känn dig fri att testa andra aspekter av klassen.

Steg 3: Testa presentatören

Låt oss nu fokusera på att testa presentatören. Vi behöver också Robolectric för detta test för att utnyttja flera Android-klasser, till exempel AsyncTask. Konfigurationen är mycket lik modelltestet. Vi använder View och Model mocks för att verifiera metodsamtal och definiera returvärden.

@RunWith (RobolectricGradleTestRunner.class) @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") allmän klass MainPresenterTest privat MainPresenter mPresenter; privat MainModel mockModel; privat MVP_Main.RequiredViewOps mockView; // För att testa presentatorn kan du bara // skapa objektet och skicka modell och visa mocks @ Före public void setup () // Skapa mocks mockView = Mockito.mock (MVP_Main.RequiredViewOps.class); mockModel = Mockito.mock (MainModel.class, RETURNS_DEEP_STUBS); // Passera mocks till en Presenter-instans mPresenter = ny MainPresenter (mockView); mPresenter.setModel (mockModel); // Definiera värdet som ska returneras av Model // när du laddar data när (mockModel.loadData ()). ThenReturn (true); reset (mockView); 

För att testa presentatörens metoder, låt oss börja med clickNewNote () operation, som ansvarar för att skapa en ny anteckning och registrera den i databasen med hjälp av en AsyncTask.

@Test public void testClickNewNote () // Vi måste mocka en EditText EditText mockEditText = Mockito.mock (EditText.class, RETURNS_DEEP_STUBS); // mock bör returnera en sträng när (mockEditText.getText () .String ()) .DetReturn ("Test_true"); // Vi definierar också en falsk position som ska returneras // med insertNote-metoden i Model int arrayPos = 10, när (mockModel.insertNote (any (Note.class))). ThenReturn (arrayPos); mPresenter.clickNewNote (mockEditText); verifiera (mockModel) .insertNote (any (Note.class)); verifiera (mockView) .notifyItemInserted (eq (arrayPos + 1)); verifiera (mockView) .notifyItemRangeChanged (eq (arrayPos), anyInt ()); verifiera (mockView, never ()) showToast (any (Toast.class));

Vi kan också testa ett scenario där insertNote () Metoden returnerar ett fel.

@Test public void testClickNewNoteError () EditText mockEditText = Mockito.mock (EditText.class, RETURNS_DEEP_STUBS); när (mockModel.insertNote (alla (Note.class))) thenReturn (-1).; när (mockEditText.getText () toString ().) thenReturn ( "Test_false."); när (mockModel.insertNote (alla (Note.class))) thenReturn (-1).; mPresenter.clickNewNote (mockEditText); kontrollera (mockView) .showToast (alla (Toast.class)); 

Slutligen testar vi deleteNote () metod, med tanke på både ett framgångsrikt och ett misslyckat resultat.

@Test public void testDeleteNote () när (mockModel.deleteNote (any (Note.class), anyInt ())). ThenReturn (true); int adapterPos = 0; int layoutPos = 1; mPresenter.deleteNote (ny anteckning (), adapterPos, layoutPos); kontrollera (mockView) .showProgress (); verifiera (mockModel) .deleteNote (any (Note.class), eq (adapterPos)); kontrollera (mockView) .hideProgress (); kontrollera (mockView) .notifyItemRemoved (eq (layoutPos)); kontrollera (mockView) .showToast (alla (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 anteckning (), adapterPos, layoutPos); kontrollera (mockView) .showProgress (); verifiera (mockModel) .deleteNote (any (Note.class), eq (adapterPos)); kontrollera (mockView) .hideProgress (); kontrollera (mockView) .showToast (alla (Toast.class)); 

2. Beroende på injektion med dagger 2

Dependency Injection är ett bra verktyg för utvecklare. Om du inte är bekant med beredskapsinjektion rekommenderar jag starkt att du läser Kerrys artikel om ämnet.

Dependensinsprutning är en typ av objektkonfiguration där ett objekts fält och samarbetspartners ställs in av en extern enhet. Med andra ord konfigureras objekt av en extern enhet. Dependensinjektion är ett alternativ till att objektet konfigurerar sig själv. - Jakob Jenkov

I det här exemplet tillåter beroendet injektionen att modellen och presentatorn skapas utanför visningen, vilket gör MVP-lagren mer löst kopplade och ökar separationen av problem.

Vi använder Dagger 2, ett fantastiskt bibliotek från Google, för att hjälpa oss med beroendet. Medan installationen är rak, har dagger 2 massor av coola alternativ och det är ett relativt komplext bibliotek.

Vi koncentrerar oss bara på de relevanta delarna av biblioteket för att genomföra MVP och kommer inte att täcka biblioteket i mycket detalj. Om du vill lära dig mer om Dagger, läs Kerrys handledning eller dokumentationen från Google.

Steg 1: Ställa in Dagger 2

Börja med att uppdatera projektets build.gradle fil genom att lägga till ett beroende.

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

Därefter redigerar du projektets build.dagger filen som visas nedan.

tillämpa plugin: 'com.neenbedankt.android-apt' beroenden // apt kommando kommer från android-apt plugin apt 'com.google.dagger: dagger-compiler: 2.0.2 "compile" com.google.dagger: dagger : 2.0.2 "tillhandahålls" org.glassfish: javax.annotation: 10.0-b28 '// ...

Synkronisera projektet och vänta på att operationen ska slutföras.

Steg 2: Implementera MVP med Dagger 2

Låt oss börja med att skapa en @Omfattning för Aktivitet klasser. Skapa en @anteckning med räckviddens namn.

@Scope public @interface ActivityScope 

Därefter arbetar vi på en @Modul för Huvudaktivitet. Om du har flera aktiviteter ska du ge en @Modul för varje Aktivitet.

@Module public class MainActivityModule privat MainActivity aktivitet; allmän MainActivityModule (MainActivity Activity) this.activity = aktivitet;  @Provides @ActivityScope MainActivity providesMainActivity () returnera aktivitet;  @Provides @ActivityScope MVP_Main.ProvidedPresenterOps providedPresenterOps () MainPresenter presenter = ny MainPresenter (aktivitet); MainModel model = ny MainModel (presentatör); presenter.setModel (modell); returleverantör 

Vi behöver också en @Subcomponent att skapa en bro med vår ansökan @Komponent, som vi fortfarande behöver skapa.

@ActivityScope @Subcomponent (modules = MainActivityModule.class) offentliga gränssnitt MainActivityComponent MainActivity injector (MainActivity activity); 

Vi måste skapa en @Modul och a @Komponent för Ansökan.

@Module public class AppModule privat Application application; offentlig AppModule (Application Application) this.application = application;  @Provides @Singleton public Application providesApplication () returnera ansökan; 
@Singleton @Component (modules = AppModule.class) offentligt gränssnitt AppComponent Application application (); MainActivityComponent getMainComponent (MainActivityModule-modulen); 

Slutligen behöver vi en Ansökan klass för att initiera beroendet av beroendet.

public class SampleApp utökar Applikation public static SampleApp get (Context context) return (SampleApp) context.getApplicationContext ();  @Override public void onCreate () super.onCreate (); initAppComponent ();  Privat AppComponent appComponent; privat tomt initAppComponent () appComponent = DaggerAppComponent.builder () .appModule (ny AppModule (this)) .build ();  offentlig AppComponent getAppComponent () return appComponent; 

Glöm inte att inkludera klassnamnet i projektets manifest.

Steg 3: Injicera MVP-klasser

Slutligen kan vi @Injicera våra MVP klasser. De förändringar som vi behöver göra görs i Huvudaktivitet klass. Vi ändrar hur modell och presentatör initieras. Det första steget är att ändra MVP_Main.ProvidedPresenterOps variabel deklaration. Det måste vara offentlig och vi måste lägga till en @Injicera anteckning.

@Inject public MVP_Main.ProvidedPresenterOps mPresenter;

För att ställa in MainActivityComponent, lägg till följande:

/ ** * Ställ in @link com.tinmegali.tutsmvp_sample.di.component.MainActivityComponent * för att inställa och injicera en @link MainPresenter * / privat void setupComponent () Log.d (TAG, "setupComponent") ; SampleApp.get (this) .getAppComponent () .getMainComponent (new MainActivityModule (this)) .inject (this); 

Allt vi behöver göra nu är att initiera eller initialisera presentatören, beroende på dess tillstånd på StateMaintainer. Ändra setupMVP () metod och lägg till följande:

/ ** * Inställningsmodell Visa presentationsmönster. * Använd en @link StateMaintainer för att behålla * Presenter och Model instanser mellan konfigurationsändringar. * / private void setupMVP () om mStateMaintainer.firstTimeIn ()) initiera ();  annars reinitialize ();  / ** * Ställ in injektionen @link MainPresenter och spara i mStateMaintainer * / privat tomt initiera () Log.d (TAG, "initialisera"); setupComponent (); mStateMaintainer.put (MainPresenter.class.getSimpleName (), mPresenter);  / ** * Återställ @link MainPresenter från mStateMaintainer eller skapar * en ny @link MainPresenter om förekomsten har gått vilse från mStateMaintainer * / privat ogiltigt återinaktivera () Log.d (TAG, "reinitialize"); mPresenter = mStateMaintainer.get (MainPresenter.class.getSimpleName ()); mPresenter.setView (detta); om (mPresenter == null) setupComponent (); 

MVP-elementen konfigureras nu oberoende av View. Koden är mer organiserad tack vare användningen av beroendet. Du kan förbättra din kod ännu mer med hjälp av beroendeinsprutning för att injicera andra klasser, till exempel DAO.

3. Undvik gemensamma problem

Jag har listat ett antal vanliga problem som du borde undvika när du använder modellvisningsmönster.

  • Kontrollera alltid om visningen är tillgänglig innan du ringer den. Vyn är knuten till programmets livscykel och kan förstöras vid din begäran.
  • Glöm inte att skicka en ny referens från Visa när den återskapas.
  • Ring upp onDestroy () i presentatören varje gång visningen förstörs. I vissa fall kan det vara nödvändigt att informera presentatören om en onStop eller en onPause händelse.
  • Överväg att använda flera presentatörer när du arbetar med komplexa visningar.
  • När du använder flera presentatörer är det enklaste sättet att skicka information mellan dem genom att anta någon form av händelsebuss.
  • För att behålla ditt visningsskikt så passivt som möjligt kan du överväga att använda beroendeinsprutning för att skapa presentations- och modelllagren utanför visningen.

Slutsats

Du nådde slutet av den här serien där vi undersökte modellvisningsmönster. Du borde nu kunna implementera MVP-mönstret i dina egna projekt, testa det och till och med anta beroendeinsprutning. Jag hoppas att du har haft den här resan så mycket som jag gjorde. hoppas vi ses snart.