Testen en afhankelijkheid Injectie met presentator modelweergave op Android

We verkenden de concepten van het Model View Presenter-patroon in het eerste deel van deze serie en we implementeerden onze eigen versie van het patroon in het tweede deel. Het is nu tijd om een ​​beetje dieper te graven. In deze zelfstudie concentreren we ons op de volgende onderwerpen:

  • het opzetten van de testomgeving en schrijfeenheidstests voor de MVP-klassen
  • het implementeren van het MVP-patroon met afhankelijkheidsinjectie met Dagger 2
  • we bespreken veelvoorkomende problemen die voorkomen moeten worden bij het gebruik van MVP op Android

1. Eenheidstesten

Een van de grootste voordelen van het adopteren van het MVP-patroon is dat het unit testing vereenvoudigt. Laten we dus testen schrijven voor de klassen Model en Presenter die we in het laatste deel van deze serie hebben gemaakt en geïmplementeerd. We zullen onze tests uitvoeren met Robolectric, een unit-testraamwerk dat veel handige tips biedt voor Android-klassen. Om mock-objecten te maken, zullen we Mockito gebruiken, waarmee we kunnen verifiëren of bepaalde methoden zijn aangeroepen.

Stap 1: Setup

Bewerk de build.gradle bestand van uw app-module en voeg de volgende afhankelijkheden toe.

afhankelijkheden // ... testCompile 'junit: junit: 4.12' // Stel deze afhankelijkheid in als u Hamcrest matching testCompile wilt gebruiken 'org.hamcrest: hamcrest-library: 1.1' testCompile 'org.robolectric: robolectric: 3.0 "testCompile' org .mockito: mockito-core: 1.10.19 '

Binnen in het project src map, maak de volgende mapstructuur test / java / [package-name] / [app-naam]. Maak vervolgens een foutopsporingsconfiguratie om de testsuite uit te voeren. Klik Configuraties bewerken ...  op de top.

Klik op de + knop en selecteer JUnit van de lijst.

set Werkmap naar $ MODULE_DIR $.

We willen dat deze configuratie alle unit-tests uitvoert. set Soort test naar Alles in pakket en voer de pakketnaam in de Pakket veld-.

Stap 2: Het model testen

Laten we onze tests beginnen met de klasse Model. De eenheidstest wordt uitgevoerd met RobolectricGradleTestRunner.class, die de nodige middelen biedt om Android-specifieke operaties te testen. Het is belangrijk om te annoteren @Cofing met de volgende opties:

@RunWith (RobolectricGradleTestRunner.class) // Wijzig wat nodig is voor uw project @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") public class MainModelTest // schrijf de testen

We willen een echt DAO (data access object) gebruiken om te testen of de gegevens correct worden verwerkt. Om toegang te krijgen tot een Context, wij gebruiken de RuntimeEnvironment.application klasse.

privé DAO mDAO; // Om het Model te testen, kunt u // het object maken en // een Presenter-mock en een DAO-instantie @ doorgaan voor de installatie van de openbare ruimte () // Door RuntimeEnvironment.application te gebruiken, krijgt // ons toegang tot een context en maak een echte DAO // invoegen van gegevens die tijdelijk worden opgeslagen Context context = RuntimeEnvironment.application; mDAO = nieuwe DAO (context); // Een spot gebruiken Presenter zal toestaan ​​om // te verifiëren als bepaalde methoden zijn aangeroepen in Presenter MainPresenter mockPresenter = Mockito.mock (MainPresenter.class); // We maken een Modelinstantie met een constructie die // een DAO bevat. Deze constructor bestaat om tests te vergemakkelijken mModel = new MainModel (mockPresenter, mDAO); // Abonneren mNotes is nodig voor testmethoden // die afhangt van de arrayList mModel.mNotes = new ArrayList <> (); // We stellen onze mock-presentator opnieuw in om te garanderen dat // onze methode-verificatie consistent blijft tussen de testreset (mockPresenter); 

Het is nu tijd om de methoden van het model te testen.

// Maak een Note-object om te gebruiken in de tests private Note createNote (String-tekst) Note note = new Note (); note.setText (tekst); note.setDate ("sommige datum"); terugkeer notitie;  // Controleer loadData @Test public void loadData () int notesSize = 10; // gegevens direct invoegen met DAO voor (int i = 0; i -1);  // Verify deleteNote @Test public void deleteNote () // We moeten een notitie toevoegen in DB Note note = createNote ("testNote"); Note placedNote = mDAO.insertNote (opmerking); // voeg dezelfde notitie toe in mNotes ArrayList mModel.mNotes = new ArrayList <> (); mModel.mNotes.add (insertedNote); // verifieer of deleteNote de juiste resultaten oplevert assertTrue (mModel.deleteNote (placedNote, 0)); Opmerking fakeNote = createNote ("fakeNote"); assertFalse (mModel.deleteNote (fakeNote, 0)); 

U kunt nu de modeltest uitvoeren en de resultaten controleren. Voel je vrij om andere aspecten van de klas te testen.

Stap 3: De presentator testen

Laten we ons nu richten op het testen van de presentator. We hebben ook Robolectric nodig voor deze test om gebruik te maken van verschillende Android-klassen, zoals AsyncTask. De configuratie lijkt erg op de modeltest. We gebruiken View en Model mocks om methodeaanroepen te verifiëren en retourwaarden te definiëren.

@RunWith (RobolectricGradleTestRunner.class) @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") public class MainPresenterTest private MainPresenter mPresenter; privé MainModel mockModel; private MVP_Main.RequiredViewOps mockView; // Om de Presenter te testen, kunt u // het object maken en het model doorgeven en mocks bekijken @Before public void setup () // Creating the mocks mockView = Mockito.mock (MVP_Main.RequiredViewOps.class); mockModel = Mockito.mock (MainModel.class, RETURNS_DEEP_STUBS); // Geef de moppen door aan een Presenter-instantie mPresenter = new MainPresenter (mockView); mPresenter.setModel (mockModel); // Definieer de waarde die moet worden geretourneerd door Model // bij het laden van gegevens wanneer (mockModel.loadData ()). ThenReturn (true); reset (mockView); 

Laten we beginnen met de methode om de methoden van de presentator te testen clickNewNote () bewerking, die verantwoordelijk is voor het maken van een nieuwe notitie en deze in de database registreert met behulp van een AsyncTask.

@Test public void testClickNewNote () // We moeten een EditText EditText bespotten mockEditText = Mockito.mock (EditText.class, RETURNS_DEEP_STUBS); // de mock zou een String terug moeten geven als (mockEditText.getText (). toString ()). thenReturn ("Test_true"); // we definiëren ook een neppositie die geretourneerd moet worden // door de insertNote-methode in Model int arrayPos = 10; when (mockModel.insertNote (elke (Note.class))). ThenReturn (arrayPos); mPresenter.clickNewNote (mockEditText); verify (mockModel) .insertNote (elke (Note.class)); verify (mockView) .notifyItemInserted (eq (arrayPos + 1)); verify (mockView) .notifyItemRangeChanged (eq (arrayPos), anyInt ()); verify (mockView, never ()). showToast (any (Toast.class));

We zouden ook een scenario kunnen testen waarin de insertNote () methode retourneert een fout.

@Test public void testClickNewNoteError () EditText mockEditText = Mockito.mock (EditText.class, RETURNS_DEEP_STUBS); wanneer (mockModel.insertNote (eventuele (Note.class))) thenReturn (-1).; wanneer (mockEditText.getText () toString ().) thenReturn ( "Test_false."); wanneer (mockModel.insertNote (eventuele (Note.class))) thenReturn (-1).; mPresenter.clickNewNote (mockEditText); verifiëren (mockView) .showToast (eventuele (Toast.class)); 

Eindelijk testen we deleteNote () methode, waarbij zowel een succesvol als een niet succesvol resultaat wordt overwogen.

@Test public void testDeleteNote () when (mockModel.deleteNote (any (Note.class), anyInt ())). ThenReturn (true); int adapterPos = 0; int layoutPos = 1; mPresenter.deleteNote (nieuwe Note (), adapterPos, layoutPos); verifiëren (mockView) .showProgress (); verifieer (mockModel) .deleteNote (elke (Note.class), eq (adapterPos)); verifiëren (mockView) .hideProgress (); verifiëren (mockView) .notifyItemRemoved (eq (layoutPos)); verifiëren (mockView) .showToast (eventuele (Toast.class));  @Test public void testDeleteNoteError () when (mockModel.deleteNote (any (Note.class), anyInt ())). ThenReturn (false); int adapterPos = 0; int layoutPos = 1; mPresenter.deleteNote (nieuwe Note (), adapterPos, layoutPos); verifiëren (mockView) .showProgress (); verifieer (mockModel) .deleteNote (elke (Note.class), eq (adapterPos)); verifiëren (mockView) .hideProgress (); verifiëren (mockView) .showToast (eventuele (Toast.class)); 

2. Afhankelijkheid Injectie met Dagger 2

Dependency Injection is een geweldige tool die beschikbaar is voor ontwikkelaars. Als u niet bekend bent met afhankelijkheidsinjectie, raad ik u ten zeerste aan Kerry's artikel over het onderwerp te lezen.

Injectie van afhankelijkheid is een stijl van objectconfiguratie waarin de velden en bijdragers van een object worden ingesteld door een externe entiteit. Met andere woorden, objecten worden geconfigureerd door een externe entiteit. Injectie door afhankelijkheid is een alternatief voor het zelf configureren van het object. - Jakob Jenkov

In dit voorbeeld laat afhankelijkheidsinjectie toe dat het model en de presentator buiten de weergave worden gemaakt, waardoor de MVP-lagen losser worden gekoppeld en de scheiding van zorgen wordt vergroot.

We gebruiken Dagger 2, een geweldige bibliotheek van Google, om ons te helpen met afhankelijkheidsinjectie. Hoewel de installatie eenvoudig is, heeft dagger 2 veel coole opties en is het een relatief complexe bibliotheek.

We concentreren ons alleen op de relevante delen van de bibliotheek om MVP te implementeren en zullen de bibliotheek niet in detail beschrijven. Als je meer wilt weten over Dagger, lees dan de handleiding van Kerry of de documentatie van Google.

Stap 1: Dagger 2 instellen

Begin met het bijwerken van de projecten build.gradle bestand door een afhankelijkheid toe te voegen.

afhankelijkheden // ... classpath 'com.neenbedankt.gradle.plugins: android-apt: 1.8'

Bewerk vervolgens de projecten build.dagger bestand zoals hieronder getoond.

plug-in toepassen: 'com.neenbedankt.android-apt'-afhankelijkheden // apt-opdracht komt van de android-apt-plugin apt' com.google.dagger: dagger-compiler: 2.0.2 'compileer' com.google.dagger: dagger : 2.0.2 'provided' org.glassfish: javax.annotation: 10.0-b28 '// ...

Synchroniseer het project en wacht tot de bewerking is voltooid.

Stap 2: MVP implementeren met Dagger 2

Laten we beginnen met het maken van een @Scope voor de Activiteit klassen. Maak een @annotation met de naam van de scope.

@Scope public @interface ActivityScope 

Vervolgens werken we aan een @Module voor de Hoofdactiviteit. Als u meerdere activiteiten heeft, moet u een @Module voor elk Activiteit.

@Module public class MainActivityModule private MainActivity-activiteit; public MainActivityModule (MainActivity-activiteit) this.activity = activity;  @ Verstrekt @ActivityScope MainActivity biedt MainActivity () teruggaande activiteit;  @Provides @ActivityScope MVP_Main.ProvidedPresenterOps providedPresenterOps () MainPresenter presenter = new MainPresenter (activity); MainModel-model = nieuw MainModel (presentator); presenter.setModel (model); terugkeer presentator; 

We hebben ook een @Subcomponent om een ​​brug te maken met onze applicatie @Component, die we nog moeten maken.

@ActivityScope @Subcomponent (modules = MainActivityModule.class) openbare interface MainActivityComponent MainActivity inject (MainActivity-activiteit); 

We moeten een maken @Module en een @Component voor de Toepassing.

@Module public class AppModule private Application application; openbare AppModule (Applicatie-applicatie) this.application = application;  @Provides @Singleton public Application providesApplication () return application; 
@Singleton @Component (modules = AppModule.class) openbare interface AppComponent Application application (); MainActivityComponent getMainComponent (MainActivityModule-module); 

Eindelijk hebben we een Toepassing klasse om de afhankelijkheidsinjectie te initialiseren.

public class SampleApp breidt Application uit public static SampleApp get (contextcontext) return (SampleApp) context.getApplicationContext ();  @Override public void onCreate () super.onCreate (); initAppComponent ();  private AppComponent appComponent; private void initAppComponent () appComponent = DaggerAppComponent.builder () .appModule (nieuwe AppModule (this)) .build ();  openbare AppComponent getAppComponent () return appComponent; 

Vergeet niet om de klassenaam in het manifest van het project op te nemen.

Stap 3: MVP-klassen injecteren

Eindelijk kunnen we dat @Injecteren onze MVP-klassen. De wijzigingen die we moeten aanbrengen, worden gedaan in de Hoofdactiviteit klasse. We veranderen de manier waarop het model en de presentator worden geïnitialiseerd. De eerste stap is om het MVP_Main.ProvidedPresenterOps variabele verklaring. Het moet zo zijn openbaar en we moeten een toevoegen @Injecteren aantekening.

@Inject public MVP_Main.ProvidedPresenterOps mPresenter;

Om de MainActivityComponent, voeg het volgende toe:

/ ** * Configureer de @link com.tinmegali.tutsmvp_sample.di.component.MainActivityComponent * om een ​​@link MainPresenter * / private void setupComponent () Log.d (TAG, "setupComponent") te instantiëren en te injecteren ; SampleApp.get (dit) .getAppComponent () .getMainComponent (nieuwe MainActivityModule (this)) .inject (this); 

Het enige dat we nu hoeven te doen, is de Presenter initialiseren of opnieuw initialiseren, afhankelijk van de status ervan StateMaintainer. Verander de setupMVP () methode en voeg het volgende toe:

/ ** * Setup Modelweergave Presenter-patroon. * Gebruik een @link StateMaintainer om de instanties * Presenter en Model tussen configuratiewijzigingen te behouden. * / private void setupMVP () if (mStateMaintainer.firstTimeIn ()) initialize ();  else herinitialize ();  / ** * Stel de @link MainPresenter -injectie in en sla deze op mStateMaintainer * / private void initialize () Log.d (TAG, "initialize"); setupComponent (); mStateMaintainer.put (MainPresenter.class.getSimpleName (), mPresenter);  / ** * Herstel @link MainPresenter uit mStateMaintainer of maakt * een nieuwe @link MainPresenter als het exemplaar verloren is gegaan mStateMaintainer * / private leegte opnieuw initialiseren () log.d (TAG, "opnieuw initialiseren"); mPresenter = mStateMaintainer.get (MainPresenter.class.getSimpleName ()); mPresenter.setView (deze); if (mPresenter == null) setupComponent (); 

De MVP-elementen worden nu onafhankelijk van de weergave geconfigureerd. De code is overzichtelijker dankzij het gebruik van afhankelijkheidsinjectie. U kunt uw code nog verder verbeteren met afhankelijkheidsinjectie om andere klassen te injecteren, zoals DAO.

3. Algemene problemen vermijden

Ik heb een aantal veelvoorkomende problemen opgesomd die u moet vermijden bij het gebruik van het Model View Presenter-patroon.

  • Controleer altijd of de weergave beschikbaar is voordat u deze oproept. De weergave is gekoppeld aan de levenscyclus van de toepassing en kan op het moment van uw aanvraag worden vernietigd.
  • Vergeet niet om een ​​nieuwe referentie door te geven vanuit de weergave wanneer deze opnieuw wordt gemaakt.
  • telefoontje onDestroy () in de Presenter elke keer dat de weergave wordt vernietigd. In sommige gevallen kan het nodig zijn om de presentator te informeren over een onStop of een onPause evenement.
  • Overweeg om meerdere presentatoren te gebruiken bij het werken met complexe weergaven.
  • Bij het gebruik van meerdere presenters is de eenvoudigste manier om informatie tussen hen door te geven, door een soort gebeurtenisbus in te voeren.
  • Als u uw weergavelaag zo passief mogelijk wilt houden, kunt u afhankelijkheidsinjectie gebruiken om de presentator- en modellagen buiten de weergave te maken.

Conclusie

U bereikte het einde van deze serie waarin we het Model View Presenter-patroon verkenden. Je zou nu in staat moeten zijn om het MVP-patroon in je eigen projecten te implementeren, het te testen en zelfs afhankelijkheidsinjectie aan te nemen. Ik hoop dat je net zoveel van deze reis hebt genoten als ik. ik hoop je snel te zien.