Pruebas e inyección de dependencia con Model View Presenter en Android

Exploramos los conceptos del patrón Model View Presenter en la primera parte de esta serie e implementamos nuestra propia versión del patrón en la segunda parte. Ahora es el momento de profundizar un poco más. En este tutorial, nos centramos en los siguientes temas:

  • configurando el entorno de prueba y escribiendo pruebas unitarias para las clases de MVP
  • Implementando el patrón MVP usando inyección de dependencia con Dagger 2
  • Discutimos problemas comunes que debemos evitar al usar MVP en Android

1. Pruebas unitarias

Una de las mayores ventajas de adoptar el patrón MVP es que simplifica las pruebas unitarias. Entonces, escribamos pruebas para las clases de Modelo y Presentador que creamos e implementamos en la última parte de esta serie. Ejecutaremos nuestras pruebas utilizando Robolectric, un marco de prueba unitaria que proporciona muchos apéndices útiles para las clases de Android. Para crear objetos simulados, usaremos Mockito, que nos permite verificar si se han llamado ciertos métodos..

Paso 1: Configuración

Editar el construir.gradle archivo de su módulo de aplicación y agregue las siguientes dependencias.

dependencies //… testCompile 'junit: junit: 4.12' // Establezca esta dependencia si desea usar el test de Hamcrest testCompile 'org.hamcrest: hamcrest-library: 1.1' testCompile "org.robolectric: robolectric: 3.0" testCompile 'org .mockito: mockito-core: 1.10.19 '

Dentro del proyecto src carpeta, crear la siguiente estructura de carpetas prueba / java / [nombre-paquete] / [nombre-aplicación]. A continuación, cree una configuración de depuración para ejecutar el conjunto de pruebas. Hacer clic Editar configuraciones ...  en la cima.

Haga clic en el + botón y seleccione JUIT de la lista.

Conjunto Directorio de trabajo a $ MODULE_DIR $.

Queremos que esta configuración ejecute todas las pruebas unitarias. Conjunto Tipo de pruebaTodo en paquete e ingrese el nombre del paquete en el Paquete campo.

Paso 2: Probando el modelo

Comencemos nuestras pruebas con la clase Modelo. La prueba unitaria se ejecuta utilizando RobolectricGradleTestRunner.class, que proporciona los recursos necesarios para probar las operaciones específicas de Android. Es importante anotar @Cofing con las siguientes opciones:

@RunWith (RobolectricGradleTestRunner.class) // Cambie lo que sea necesario para su proyecto @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") clase pública MainModelTest // escriba el pruebas

Queremos utilizar un DAO real (objeto de acceso a datos) para probar si los datos se están manejando correctamente. Para acceder a un Contexto, usamos el RuntimeEnvironment.application clase.

DAO privado mDAO; // Para probar el modelo, simplemente // puede crear el objeto y pasar // un simulacro de presentador y una instancia de DAO @Antes de public void setup () // El uso de RuntimeEnvironment.application nos permitirá a // acceder a un contexto y cree un DAO real // insertando datos que se guardarán temporalmente Context context = RuntimeEnvironment.application; mDAO = nuevo DAO (contexto); // El uso de un Presentador simulado permitirá verificar // si se llamaron ciertos métodos en el Presenter MainPresenter mockPresenter = Mockito.mock (MainPresenter.class); // Creamos una instancia de modelo utilizando una construcción que incluye // un DAO. Este constructor existe para facilitar las pruebas mModel = new MainModel (mockPresenter, mDAO); // Es necesario suscribirse a mNotes para los métodos de prueba // que dependen de arrayList mModel.mNotes = new ArrayList <> (); // Estamos reiniciando nuestro simulador de presentador para garantizar que // la verificación de nuestro método permanezca consistente entre los reinicios de las pruebas (mockPresenter); 

Ahora es el momento de probar los métodos del modelo..

// Crear un objeto Note para usar en las pruebas private Note createNote (String text) Note note = new Note (); note.setText (texto); note.setDate ("alguna fecha"); nota de retorno  // Verificar loadData @Test public void loadData () int notesSize = 10; // insertar datos directamente usando DAO para (int i = 0; i -1);  // Verificar deleteNote @Test public void deleteNote () // Necesitamos agregar una nota en DB Note note = createNote ("testNote"); Nota insertada = mDAO.insertNota (nota); // agregar la misma nota dentro de mNotes ArrayList mModel.mNotes = new ArrayList <> (); mModel.mNotes.add (insertNote); // verifique si deleteNote devuelve los resultados correctos assertTrue (mModel.deleteNote (InsertNote, 0)); Tenga en cuenta fakeNote = createNote ("fakeNote"); assertFalse (mModel.deleteNote (fakeNote, 0)); 

Ahora puede ejecutar la prueba del modelo y comprobar los resultados. Siéntete libre de probar otros aspectos de la clase..

Paso 3: Probando el presentador

Centrémonos ahora en probar al presentador. También necesitamos Robolectric para esta prueba para hacer uso de varias clases de Android, como AsyncTask. La configuración es muy similar a la prueba del modelo. Usamos las simulaciones de vista y modelo para verificar las llamadas de método y definir valores de retorno.

@RunWith (RobolectricGradleTestRunner.class) @Config (constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") clase pública MainPresenterTest private MainPresenter mPresenter; MainModel privado mockModel; privado MVP_Main.RequiredViewOps mockView; // Para probar el Presentador, simplemente puede // crear el objeto y pasar las simulaciones del modelo y de la vista @Antes de public void setup () // Crear las simulaciones mockView = Mockito.mock (MVP_Main.RequiredViewOps.class); mockModel = Mockito.mock (MainModel.class, RETURNS_DEEP_STUBS); // Pasar las simulaciones a una instancia de Presenter mPresenter = new MainPresenter (mockView); mPresenter.setModel (mockModel); // Defina el valor que devolverá el Modelo // al cargar los datos cuando (mockModel.loadData ()). ThenReturn (true); restablecer (mockView); 

Para probar los métodos del presentador, comencemos con el haga clic enNotaNota () operación, que es responsable de crear una nueva nota y registrarla en la base de datos usando un AsyncTask.

@Test público void testClickNewNote () // Necesitamos simular un EditText EditText mockEditText = Mockito.mock (EditText.class, RETURNS_DEEP_STUBS); // el simulacro debería devolver una cadena cuando (mockEditText.getText (). toString ()). thenReturn ("Test_true"); // también definimos una posición falsa para que sea devuelta // por el método insertNote en el Modelo int arrayPos = 10; when (mockModel.insertNote (any (Note.class)). ThenReturn (arrayPos); mPresenter.clickNewNote (mockEditText); Verify (mockModel) .insertNote (any (Note.class)); (eq (arrayPos + 1)); Verify (mockView) .notifyItemRangeChanged (eq (arrayPos), anyInt ()); Verify (mockView, never ()). showToast (any (Toast.class));

También podríamos probar un escenario en el que el insertNote () método devuelve un error.

@Test público void testClickNewNoteError () EditText mockEditText = Mockito.mock (EditText.class, RETURNS_DEEP_STUBS); when (mockModel.insertNote (any (Note.class)). thenReturn (-1); when (mockEditText.getText (). toString ()). thenReturn ("Test_false"); when (mockModel.insertNote (any (Note.class)). thenReturn (-1); mPresenter.clickNewNote (mockEditText); verificar (mockView) .showToast (any (Toast.class)); 

Finalmente, probamos borrar nota() Método, considerando tanto un resultado exitoso como un fracaso..

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

2. Dependencia Inyección Con Daga 2.

La inyección de dependencia es una gran herramienta disponible para los desarrolladores. Si no está familiarizado con la inyección de dependencia, le recomiendo que lea el artículo de Kerry sobre el tema..

La inyección de dependencia es un estilo de configuración de objeto en el que los campos y los colaboradores de un objeto están configurados por una entidad externa. En otras palabras, los objetos están configurados por una entidad externa. La inyección de dependencia es una alternativa a que el objeto se configure por sí mismo. - Jakob Jenkov

En este ejemplo, la inyección de dependencia permite que el Modelo y el Presentador se creen fuera de la Vista, lo que hace que las capas de MVP se acoplen de forma más libre y aumente la separación de preocupaciones..

Usamos Dagger 2, una increíble biblioteca de Google, para ayudarnos con la inyección de dependencia. Si bien la configuración es sencilla, la daga 2 tiene muchas opciones geniales y es una biblioteca relativamente compleja..

Nos concentramos solo en las partes relevantes de la biblioteca para implementar MVP y no cubriremos la biblioteca con mucho detalle. Si desea obtener más información sobre Dagger, lea el tutorial de Kerry o la documentación proporcionada por Google.

Paso 1: Configuración de la Daga 2

Comience por actualizar el proyecto construir.gradle archivo añadiendo una dependencia.

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

A continuación, edite el proyecto build.dagger archivo como se muestra a continuación.

aplique el complemento: 'com.neenbedankt.android-apt' dependencies // el comando apt viene del complemento android-apt apt 'com.google.dagger: dagger-compiler: 2.0.2' compile 'com.google.dagger: dagger : 2.0.2 'proporcionado' org.glassfish: javax.annotation: 10.0-b28 '//…

Sincronice el proyecto y espere a que la operación se complete.

Paso 2: Implementando MVP Con Dagger 2

Comencemos creando una @Alcance Para el Actividad clases Crear un @anotación con el nombre del alcance.

@Scope public @interface ActivityScope 

A continuación, trabajamos en un @Módulo Para el Actividad principal. Si tiene múltiples actividades, debe proporcionar un @Módulo para cada Actividad.

@Module public class MainActivityModule actividad MainActivity privada; mainActivityModule público (actividad MainActivity) this.activity = activity;  @Proporciona @ActivityScope MainActivity proporcionaMainActividad () actividad de retorno;  @Proporciona @ActivityScope MVP_Main.ProvidedPresenterOps proporcionadoPresenterOps () MainPresenter presenter = new MainPresenter (actividad); Modelo de MainModel = nuevo MainModel (presentador); presenter.setModel (modelo); presentador de vuelta 

También necesitamos un @Subcomponente Para crear un puente con nuestra aplicación. @Componente, que todavía necesitamos crear.

@ActivityScope @Subcomponent (modules = MainActivityModule.class) interfaz pública MainActivityComponent MainActivity inyect (actividad MainActivity); 

Tenemos que crear un @Módulo y un @Componente Para el Solicitud.

@Module public class AppModule aplicación privada; AppModule público (aplicación) this.application = application;  @Provides @Singleton public Application proveeApplication () return application; 
@Singleton @Component (modules = AppModule.class) interfaz pública AppComponent Application application (); MainActivityComponent getMainComponent (módulo MainActivityModule); 

Por último, necesitamos un Solicitud Clase para inicializar la inyección de dependencia..

la clase pública SampleApp extiende la aplicación public static SampleApp get (Context context) return (SampleApp) context.getApplicationContext ();  @Override public void onCreate () super.onCreate (); initAppComponent ();  appComponent appComponent privado; private void initAppComponent () appComponent = DaggerAppComponent.builder () .appModule (nuevo AppModule (this)) .build ();  public AppComponent getAppComponent () return appComponent; 

No olvides incluir el nombre de la clase en el manifiesto del proyecto..

Paso 3: Inyectar clases MVP

Finalmente, podemos @Inyectar nuestras clases de MVP. Los cambios que necesitamos hacer se hacen en el Actividad principal clase. Cambiamos la forma en que se inicializan el modelo y el presentador. El primer paso es cambiar la MVP_Main.ProvidedPresenterOps declaración de variable. Necesita ser público y necesitamos añadir un @Inyectar anotación.

@Inject public MVP_Main.ProvidedPresenterOps mPresenter;

Para configurar el MainActivityComponent, agregue lo siguiente:

/ ** * Configurar @link com.tinmegali.tutsmvp_sample.di.component.MainActivityComponent * para crear una instancia e inyectar un @link MainPresenter * / private void setupComponent () Log.d (TAG, "setupComponent") ; SampleApp.get (this) .getAppComponent () .getMainComponent (new MainActivityModule (this)) .inject (this); 

Todo lo que tenemos que hacer ahora es inicializar o reinicializar el Presentador, dependiendo de su estado en Mantenedor del estado. Cambiar el setupMVP () Método y añadir lo siguiente:

/ ** * Configura el patrón de Model View Presenter. * Use un @link StateMaintainer para mantener las instancias de * Presenter y Model entre los cambios de configuración. * / private void setupMVP () if (mStateMaintainer.firstTimeIn ()) initialize ();  else reinicializar ();  / ** * Configurar la inyección @link MainPresenter y guardar en mStateMaintainer * / private void initialize () Log.d (TAG, "initialize"); setupComponent (); mStateMaintainer.put (MainPresenter.class.getSimpleName (), mPresenter);  / ** * Recuperar @link MainPresenter desde mStateMaintainer o crea * un nuevo @link MainPresenter si la instancia se ha perdido de mStateMaintainer * / private void reinitialize () Log.d (TAG, "reinitialize"); mPresenter = mStateMaintainer.get (MainPresenter.class.getSimpleName ()); mPresenter.setView (esto); if (mPresenter == null) setupComponent (); 

Los elementos MVP ahora se configuran de forma independiente desde la Vista. El código está más organizado gracias al uso de inyección de dependencia. Podría mejorar su código aún más usando la inyección de dependencia para inyectar otras clases, como DAO.

3. Evitar problemas comunes

He enumerado una serie de problemas comunes que debe evitar al usar el patrón de Model View Presenter.

  • Compruebe siempre si la vista está disponible antes de llamarla. La vista está vinculada al ciclo de vida de la aplicación y podría destruirse en el momento de su solicitud.
  • No olvide pasar una nueva referencia de la Vista cuando se vuelva a crear..
  • Llamada onDestroy () en el Presentador cada vez que se destruye la Vista. En algunos casos, puede ser necesario informar al presentador sobre una onStop o un en pausa evento.
  • Considere usar varios presentadores cuando trabaje con vistas complejas.
  • Cuando se usan varios presentadores, la forma más fácil de pasar información entre ellos es mediante la adopción de algún tipo de bus de eventos.
  • Para mantener su capa de Vista lo más pasiva posible, considere usar la inyección de dependencia para crear las capas de Modelo y Presentador fuera de la Vista.

Conclusión

Llegó al final de esta serie en la que exploramos el patrón de presentador de vista de modelo. Ahora debería poder implementar el patrón MVP en sus propios proyectos, probarlo e incluso adoptar la inyección de dependencia. Espero que hayas disfrutado este viaje tanto como yo. espero verte pronto.