Iniezione dei test e delle dipendenze con Model View Presenter su Android

Abbiamo esplorato i concetti del modello Model View Presenter nella prima parte di questa serie e abbiamo implementato la nostra versione del pattern nella seconda parte. Adesso è il momento di scavare un po 'più a fondo. In questo tutorial, ci concentriamo sui seguenti argomenti:

  • impostazione dell'ambiente di test e test delle unità di scrittura per le classi MVP
  • implementare il pattern MVP utilizzando l'injection dependance con Dagger 2
  • discutiamo problemi comuni da evitare quando si utilizza MVP su Android

1. Test dell'unità

Uno dei maggiori vantaggi dell'adozione del pattern MVP è che semplifica i test unitari. Quindi scriviamo i test per le classi Model e Presenter che abbiamo creato e implementato nell'ultima parte di questa serie. Eseguiremo i nostri test utilizzando Robolectric, un framework di test unitario che fornisce molti stub utili per le classi Android. Per creare oggetti mock, useremo Mockito, che ci consente di verificare se sono stati chiamati determinati metodi.

Passaggio 1: installazione

Modifica il build.gradle file del modulo dell'app e aggiungere le seguenti dipendenze.

dipendenze // ... testCompile 'junit: junit: 4.12' // Imposta questa dipendenza se vuoi usare Hamcrest matching testCompile 'org.hamcrest: hamcrest-library: 1.1' testCompile "org.robolectric: robolectric: 3.0" testCompile 'org .mockito: mockito-core: 1.10.19 '

All'interno del progetto src cartella, crea la seguente struttura di cartelle test / java / [nome-pacchetto] / [app-name]. Quindi, creare una configurazione di debug per eseguire la suite di test. Clic Modifica configurazioni ...  in cima.

Clicca il + pulsante e selezionare JUnit dalla lista.

Impostato Directory di lavoro a $ MODULE_DIR $.

Vogliamo che questa configurazione esegua tutti i test unitari. Impostato Tipo di provaTutto nel pacchetto e inserisci il nome del pacchetto nel file Pacchetto campo.

Passaggio 2: test del modello

Iniziamo i nostri test con la classe Model. Il test dell'unità funziona usando RobolectricGradleTestRunner.class, che fornisce le risorse necessarie per testare le operazioni specifiche di Android. È importante annotare @Cofing con le seguenti opzioni:

@RunWith (RobolectricGradleTestRunner.class) // Cambia ciò che è necessario per il tuo progetto @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") classe pubblica MainModelTest // scrivi il test

Vogliamo utilizzare un vero DAO (oggetto di accesso ai dati) per verificare se i dati vengono gestiti correttamente. Per accedere a Contesto, noi usiamo il RuntimeEnvironment.application classe.

DAO privato mDAO; // Per testare il modello, è possibile // creare l'oggetto e passare // a Presenter mock e un'istanza DAO @Before public void setup () // L'utilizzo di RuntimeEnvironment.application consentirà a // us di accedere a un contesto e creare un vero DAO // inserire dati che verranno salvati temporaneamente Contesto context = RuntimeEnvironment.application; mDAO = nuovo DAO (contesto); // Usando un simulatore Presenter permetterà di verificare // se sono stati chiamati determinati metodi in Presenter MainPresenter mockPresenter = Mockito.mock (MainPresenter.class); // Creiamo un'istanza Model utilizzando una costruzione che include // un DAO. Questo costruttore esiste per facilitare i test mModel = new MainModel (mockPresenter, mDAO); // La sottoscrizione di mNotes è necessaria per i metodi test // che dipende dalla matriceList mModel.mNotes = new ArrayList <> (); // Stiamo resettando il nostro finto Presenter per garantire che // la nostra verifica del metodo rimanga coerente tra i test resettati (mockPresenter); 

È giunto il momento di testare i metodi del modello.

// Crea oggetto Note da utilizzare nei test private Note createNote (String text) Note note = new Note (); note.setText (testo); note.setDate ("some date"); nota di ritorno;  // Verifica loadData @Test public void loadData () int notesSize = 10; // inserimento di dati direttamente usando DAO per (int i = 0; i -1);  // Verifica deleteNote @Test public void deleteNote () // È necessario aggiungere una nota in DB Note note = createNote ("testNote"); Nota insertedNote = mDAO.insertNote (note); // aggiungi la stessa nota all'interno di mNotes ArrayList mModel.mNotes = new ArrayList <> (); mModel.mNotes.add (insertedNote); // verifica se deleteNote restituisce i risultati corretti assertTrue (mModel.deleteNote (insertedNote, 0)); Nota fakeNote = createNote ("fakeNote"); assertFalse (mModel.deleteNote (fakeNote, 0)); 

È ora possibile eseguire il test del modello e controllare i risultati. Sentiti libero di testare altri aspetti della classe.

Passaggio 3: test del presentatore

Concentriamoci ora sulla verifica del Presenter. Abbiamo anche bisogno di Robolectric per questo test per utilizzare diverse classi di Android, come ad esempio AsyncTask. La configurazione è molto simile al test del modello. Utilizziamo i mock View e Model per verificare le chiamate di metodo e definire i valori di ritorno.

@RunWith (RobolectricGradleTestRunner.class) @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") public class MainPresenterTest private MainPresenter mPresenter; MainModel privato mockModel; private MVP_Main.RequiredViewOps mockView; // Per testare Presenter, è possibile // creare l'oggetto e passare le mosse Model e View @Before public void setup () // Creazione delle mock mockView = Mockito.mock (MVP_Main.RequiredViewOps.class); mockModel = Mockito.mock (MainModel.class, RETURNS_DEEP_STUBS); // Passa i mock a un'istanza di Presenter mPresenter = new MainPresenter (mockView); mPresenter.setModel (mockModel); // Definisce il valore da restituire da Model // durante il caricamento dei dati quando (mockModel.loadData ()). ThenReturn (true); reset (mockView); 

Per testare i metodi del Presenter, iniziamo con clickNewNote () operazione, che è responsabile per la creazione di una nuova nota e registrarla nel database utilizzando un AsyncTask.

@Test public void testClickNewNote () // È necessario simulare un EditText EditText mockEditText = Mockito.mock (EditText.class, RETURNS_DEEP_STUBS); // il mock dovrebbe restituire una stringa quando (mockEditText.getText (). toString ()). thenReturn ("Test_true"); // definiamo anche una posizione falsa che deve essere restituita // dal metodo insertNote in Model int arrayPos = 10; quando (mockModel.insertNote (any (Note.class))). ThenReturn (arrayPos); mPresenter.clickNewNote (mockEditText); verify (mockModel) .insertNote (any (Note.class)); verify (mockView) .notifyItemInserted (eq (arrayPos + 1)); verify (mockView) .notifyItemRangeChanged (eq (arrayPos), anyInt ()); verify (mockView, never ()). showToast (any (Toast.class));

Potremmo anche testare uno scenario in cui il insertNote () metodo restituisce un errore.

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

Alla fine, testiamo deleteNote () metodo, considerando sia un risultato positivo che un risultato non riuscito.

@Test public void testDeleteNote () when (mockModel.deleteNote (any (Note.class), anyInt ())). ThenReturn (true); int adapterPos = 0; int layoutPos = 1; mPresenter.deleteNote (new Note (), adapterPos, layoutPos); verificare (mockView) .showProgress (); verificare (mockModel) .deleteNote (any (Note.class), eq (adapterPos)); verificare (mockView) .hideProgress (); verificare (mockView) .notifyItemRemoved (eq (layoutPos)); verificare (mockView) .showToast (qualsiasi (Toast.class));  @Test public void testDeleteNoteError () when (mockModel.deleteNote (any (Note.class), anyInt ())). ThenReturn (false); int adapterPos = 0; int layoutPos = 1; mPresenter.deleteNote (new Note (), adapterPos, layoutPos); verificare (mockView) .showProgress (); verificare (mockModel) .deleteNote (any (Note.class), eq (adapterPos)); verificare (mockView) .hideProgress (); verificare (mockView) .showToast (qualsiasi (Toast.class)); 

2. Iniezione delle dipendenze con Dagger 2

Dependency Injection è un ottimo strumento disponibile per gli sviluppatori. Se non hai familiarità con l'iniezione di dipendenza, ti consiglio vivamente di leggere l'articolo di Kerry sull'argomento.

L'iniezione di dipendenza è uno stile di configurazione dell'oggetto in cui i campi e i collaboratori di un oggetto sono impostati da un'entità esterna. In altre parole, gli oggetti sono configurati da un'entità esterna. L'iniezione delle dipendenze è un'alternativa all'aver configurato l'oggetto stesso. - Jakob Jenkov

In questo esempio, l'integrazione delle dipendenze consente che il modello e il relatore vengano creati all'esterno della vista, rendendo i livelli MVP più strettamente accoppiati e aumentando la separazione delle preoccupazioni.

Usiamo Dagger 2, una fantastica libreria di Google, per aiutarci con l'iniezione delle dipendenze. Mentre il setup è semplice, dagger 2 ha molte opzioni interessanti ed è una libreria relativamente complessa.

Ci concentriamo solo sulle parti rilevanti della libreria per implementare MVP e non copriremo la libreria in modo molto dettagliato. Se vuoi saperne di più su Dagger, leggi il tutorial di Kerry o la documentazione fornita da Google.

Passaggio 1: impostazione di Dagger 2

Inizia aggiornando il progetto build.gradle file aggiungendo una dipendenza.

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

Successivamente, modifica il progetto build.dagger file come mostrato di seguito.

applica plugin: dipendenze 'com.neenbedankt.android-apt' // // comando apt proviene dal plugin android-apt apt 'com.google.dagger: dagger-compiler: 2.0.2' compile 'com.google.dagger: dagger : 2.0.2 'fornito' org.glassfish: javax.annotation: 10.0-b28 '// ...

Sincronizzare il progetto e attendere il completamento dell'operazione.

Passaggio 2: implementazione di MVP con Dagger 2

Iniziamo creando un @Scopo per il Attività classi. Creare un @annotazione con il nome dello scope.

@Scope public @interface ActivityScope 

Successivamente, lavoriamo su a @Modulo per il Attività principale. Se hai più attività, dovresti fornire un @Modulo per ciascuno Attività.

@Module public class MainActivityModule attività MainActivity privata; public MainActivityModule (attività MainActivity) this.activity = activity;  @Provides @ActivityScope MainActivity providesMainActivity () return activity;  @Provides @ActivityScope MVP_Main.ProvidedPresenterOps providedPresenterOps () MainPresenter presenter = new MainPresenter (attività); Modello MainModel = new MainModel (presenter); presenter.setModel (modello); presentatore di ritorno; 

Abbiamo anche bisogno di un @Subcomponent per creare un ponte con la nostra applicazione @Componente, che dobbiamo ancora creare.

@ActivityScope @Subcomponent (modules = MainActivityModule.class) interfaccia pubblica MainActivityComponent MainActivity inject (attività MainActivity); 

Dobbiamo creare un @Modulo e a @Componente per il Applicazione.

@Module AppModule di classe pubblica Applicazione dell'applicazione privata; AppModule pubblico (Applicazione di applicazione) this.application = application;  @Provides @Singleton public Application fornisceApplication () return application; 
@Singleton @Component (modules = AppModule.class) interfaccia pubblica AppComponent Application application (); MainActivityComponent getMainComponent (modulo MainActivityModule); 

Infine, abbiamo bisogno di un Applicazione classe per inizializzare l'iniezione delle dipendenze.

la classe pubblica SampleApp estende l'applicazione public static SampleApp get (contesto Context) return (SampleApp) context.getApplicationContext ();  @Override public void onCreate () super.onCreate (); initAppComponent ();  app AppComponent privataComponent; private void initAppComponent () appComponent = DaggerAppComponent.builder () .appModule (new AppModule (this)) .build ();  public AppComponent getAppComponent () return appComponent; 

Non dimenticare di includere il nome della classe nel manifest del progetto.

Passaggio 3: Iniezione di classi MVP

Finalmente possiamo @Iniettare le nostre classi MVP. I cambiamenti che dobbiamo apportare sono fatti nel Attività principale classe. Modifichiamo il modo in cui Model e Presenter sono inizializzati. Il primo passo è cambiare il MVP_Main.ProvidedPresenterOps dichiarazione variabile. Deve essere pubblico e dobbiamo aggiungere un @Iniettare annotazione.

@Inject public MVP_Main.ProvidedPresenterOps mPresenter;

Per impostare il MainActivityComponent, aggiungere il seguente:

/ ** * Imposta @link com.tinmegali.tutsmvp_sample.di.component.MainActivityComponent * per creare un'istanza e iniettare un @link MainPresenter * / private void setupComponent () Log.d (TAG, "setupComponent") ; SampleApp.get (this) .getAppComponent () .getMainComponent (new MainActivityModule (this)) .inject (this); 

Tutto ciò che dobbiamo fare ora è inizializzare o reinizializzare il Presenter, a seconda del suo stato attivo StateMaintainer. Cambiare il setupMVP () metodo e aggiungere il seguente:

/ ** * Imposta modello Visualizza modello Presenter. * Utilizzare un @link StateMaintainer per mantenere le istanze * Presenter e Model tra le modifiche alla configurazione. * / private void setupMVP () if (mStateMaintainer.firstTimeIn ()) initialize ();  else reinizializza ();  / ** * Imposta l'iniezione @link MainPresenter e salva in mStateMaintainer * / private void initialize () Log.d (TAG, "initialize"); setupComponent (); mStateMaintainer.put (MainPresenter.class.getSimpleName (), mPresenter);  / ** * Recupera @link MainPresenter da mStateMaintainer oppure crea * un nuovo @link MainPresenter se l'istanza è stata persa mStateMaintainer * / private void reinitialize () Log.d (TAG, "reinizializza"); mPresenter = mStateMaintainer.get (MainPresenter.class.getSimpleName ()); mPresenter.setView (questo); if (mPresenter == null) setupComponent (); 

Gli elementi MVP vengono ora configurati in modo indipendente dalla vista. Il codice è più organizzato grazie all'uso dell'iniezione di dipendenza. Potresti migliorare ulteriormente il tuo codice usando l'iniezione di dipendenza per iniettare altre classi, come DAO.

3. Evitare problemi comuni

Ho elencato una serie di problemi comuni che dovresti evitare quando utilizzi il pattern Model View Presenter.

  • Controlla sempre se la vista è disponibile prima di chiamarla. La vista è legata al ciclo di vita dell'applicazione e potrebbe essere distrutta al momento della richiesta.
  • Non dimenticare di passare un nuovo riferimento dalla vista quando viene ricreato.
  • Chiamata OnDestroy () nel Presenter ogni volta che la Vista viene distrutta. In alcuni casi, può essere necessario informare il Presentatore riguardo a onStop o un onPause evento.
  • Prendi in considerazione l'utilizzo di più relatori quando lavori con viste complesse.
  • Quando si utilizzano più relatori, il modo più semplice per passare le informazioni tra di loro è l'adozione di una sorta di bus eventi.
  • Per mantenere il livello View il più passivo possibile, è consigliabile utilizzare l'iniezione delle dipendenze per creare i livelli Presenter e Model all'esterno della vista.

Conclusione

Hai raggiunto la fine di questa serie in cui abbiamo esplorato il pattern Model View Presenter. Ora dovresti essere in grado di implementare lo schema MVP nei tuoi progetti, testarlo e persino adottare l'iniezione di dipendenza. Spero che tu abbia apprezzato questo viaggio tanto quanto me. spero di vederti presto.