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:
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.
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.
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.
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));
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.
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.
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.
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 imStateMaintainer
* / privat tomt initiera () Log.d (TAG, "initialisera"); setupComponent (); mStateMaintainer.put (MainPresenter.class.getSimpleName (), mPresenter); / ** * Återställ @link MainPresenter frånmStateMaintainer
eller skapar * en ny @link MainPresenter om förekomsten har gått vilse frånmStateMaintainer
* / 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.
Jag har listat ett antal vanliga problem som du borde undvika när du använder modellvisningsmönster.
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.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.