Conseguir Dagger para inyectar objetos simulados al hacer pruebas funcionales espresso para Android

Hace poco me he vuelto loco con Dagger porque el concepto de DI tiene un sentido completo. Uno de los mejores "subproductos" de DI (como Jake Wharton puso en una de sus presentaciones) es más fácil de probar.

Así que ahora estoy básicamente usando espresso para hacer algunas pruebas funcionales, y quiero ser capaz de inyectar datos dummy / mock a la aplicación y tener la actividad mostrarlos. Supongo que ya que, esta es una de las mayores ventajas de DI, esto debería ser una pregunta relativamente simple. Por alguna razón sin embargo, no puedo parecer envolver mi cabeza alrededor de él. Cualquier ayuda sería muy apreciada. Esto es lo que tengo hasta ahora (he escrito un ejemplo que refleja mi configuración actual):

public class MyActivity extends MyBaseActivity { @Inject Navigator _navigator; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MyApplication.get(this).inject(this); // ... setupViews(); } private void setupViews() { myTextView.setText(getMyLabel()); } public String getMyLabel() { return _navigator.getSpecialText(); // "Special Text" } } 

Estos son mis módulos de daga:

 // Navigation Module @Module(library = true) public class NavigationModule { private Navigator _nav; @Provides @Singleton Navigator provideANavigator() { if (_nav == null) { _nav = new Navigator(); } return _nav; } } // App level module @Module( includes = { SessionModule.class, NavigationModule.class }, injects = { MyApplication.class, MyActivity.class, // ... }) public class App { private final Context _appContext; AppModule(Context appContext) { _appContext = appContext; } // ... } 

En mi prueba de Espresso, estoy tratando de insertar un módulo de simulación así:

 public class MyActivityTest extends ActivityInstrumentationTestCase2<MyActivity> { public MyActivityTest() { super(MyActivity.class); } @Override public void setUp() throws Exception { super.setUp(); ObjectGraph og = ((MyApplication) getActivity().getApplication()).getObjectGraph().plus(new TestNavigationModule()); og.inject(getActivity()); } public void test_SeeSpecialText() { onView(withId(R.id.my_text_view)).check(matches(withText( "Special Dummy Text))); } @Module(includes = NavigationModule.class, injects = { MyActivityTest.class, MyActivity.class }, overrides = true, library = true) static class TestNavigationModule { @Provides @Singleton Navigator provideANavigator() { return new DummyNavigator(); // that returns "Special Dummy Text" } } } 

Esto no funciona en absoluto. Mis pruebas de espresso se ejecutan, pero el TestNavigationModule es completamente ignorado … arr … 🙁

¿Qué estoy haciendo mal? ¿Existe un mejor enfoque para burlarse de los módulos con Espresso. He buscado y visto ejemplos de Robolectric, Mockito etc que se utilizan. Pero solo quiero pruebas puras de Espresso y necesito intercambiar un módulo con mi simulacro. ¿Cómo debo hacer esto?

EDITAR:

Así que fui con @ user3399328 enfoque de tener una definición de lista de módulo de prueba estática, comprobar si nulo y luego añadirlo en mi clase de aplicación. Todavía no estoy recibiendo mi versión inyectada de la clase. Tengo una sensación, sin embargo, es probablemente algo malo con la definición del módulo de prueba de daga, y no mi ciclo de vida espresso. La razón por la que estoy haciendo la suposición es que añado declaraciones de depuración y encontrar que el módulo de prueba estática no está vacía en el momento de la inyección en la clase de aplicación. ¿Podría usted señalarme una dirección de lo que podría estar haciendo mal. Aquí hay fragmentos de código de mis definiciones:

Mi aplicación:

 @Override public void onCreate() { // ... mObjectGraph = ObjectGraph.create(Modules.list(this)); // ... } 

Módulos:

 public class Modules { public static List<Object> _testModules = null; public static Object[] list(MyApplication app) { // return new Object[]{ new AppModule(app) }; List<Object> modules = new ArrayList<Object>(); modules.add(new AppModule(app)); if (_testModules == null) { Log.d("No test modules"); } else { Log.d("Test modules found"); } if (_testModules != null) { modules.addAll(_testModules); } return modules.toArray(); } } 

Módulo de prueba modificado dentro de mi clase de prueba:

 @Module(overrides = true, library = true) public static class TestNavigationModule { @Provides @Singleton Navigator provideANavigator()() { Navigator navigator = new Navigator(); navigator.setSpecialText("Dummy Text"); return navigator; } } 

Su enfoque no funciona porque sólo ocurre una vez, y como mencionó Matt, cuando se ejecuta el código de inyección real de la actividad, eliminará cualquier variable inyectada por su gráfico de objetos especiales.

Hay dos maneras de conseguir que esto funcione.

La forma rápida: crear una variable pública estática en su actividad para que una prueba pueda asignar un módulo de reemplazo y tener el código de actividad real siempre incluir este módulo si no es nulo (lo que sólo sucede en las pruebas). Es similar a mi respuesta aquí sólo para su clase base de actividad en lugar de la aplicación.

La forma más larga y probablemente mejor: refactore su código para que toda la inyección de actividad (y más importante creación de gráfico) suceda en una clase, algo como ActivityInjectHelper. En su paquete de prueba, cree otra clase llamada ActivityInjectHelper con la misma ruta del paquete que implementa los mismos métodos, excepto también más los módulos de prueba. Debido a que las clases de prueba se cargan primero, la aplicación se ejecutará con la prueba ActivityInjectHelper. De nuevo es similar a mi respuesta aquí sólo para una clase diferente.

ACTUALIZAR:

Veo que ha publicado más código y está cerca de trabajar, pero no cigarro. Tanto para las actividades como para las aplicaciones, el módulo de prueba necesita ser detectado antes de ejecutarse onCreate (). Cuando se trata de gráficos de objetos de actividad, en cualquier momento antes de la prueba getActivity () está bien. Cuando se trata de aplicaciones, es un poco más difícil porque onCreate () ya ha sido llamado por el tiempo setUp () se ejecuta. Afortunadamente, hacerlo en el constructor del test funciona – la aplicación no ha sido creada en ese momento. Menciono brevemente esto en mi primer enlace.

Con Dagger 2 y Espresso 2 las cosas han mejorado. Así es como podría ser un caso de prueba ahora. Observe que ContributorsModel es proporcionado por Dagger. La demo completa disponible aquí: https://github.com/pmellaaho/RxApp

 @RunWith(AndroidJUnit4.class) public class MainActivityTest { ContributorsModel mModel; @Singleton @Component(modules = MockNetworkModule.class) public interface MockNetworkComponent extends RxApp.NetworkComponent { } @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>( MainActivity.class, true, // initialTouchMode false); // launchActivity. @Before public void setUp() { Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); RxApp app = (RxApp) instrumentation.getTargetContext() .getApplicationContext(); MockNetworkComponent testComponent = DaggerMainActivityTest_MockNetworkComponent.builder() .mockNetworkModule(new MockNetworkModule()) .build(); app.setComponent(testComponent); mModel = testComponent.contributorsModel(); } @Test public void listWithTwoContributors() { // GIVEN List<Contributor> tmpList = new ArrayList<>(); tmpList.add(new Contributor("Jesse", 600)); tmpList.add(new Contributor("Jake", 200)); Observable<List<Contributor>> testObservable = Observable.just(tmpList); Mockito.when(mModel.getContributors(anyString(), anyString())) .thenReturn(testObservable); // WHEN mActivityRule.launchActivity(new Intent()); onView(withId(R.id.startBtn)).perform(click()); // THEN onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0)) .check(matches(hasDescendant(withText("Jesse")))); onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0)) .check(matches(hasDescendant(withText("600")))); onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1)) .check(matches(hasDescendant(withText("Jake")))); onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1)) .check(matches(hasDescendant(withText("200")))); } 

La llamada a getActivity iniciará realmente su actividad llamando a Create en el proceso, lo que significa que no obtendrá los módulos de prueba agregados a la gráfica a tiempo para ser utilizados. Usando activityInstrumentationTestcase2 no se puede realmente inyectar correctamente en el ámbito de actividad. He trabajado en torno a esto mediante el uso de mi aplicación para proporcionar dependencias a mis actividades y luego inyectar objetos simulados en el que las actividades se utilizan. No es ideal, pero funciona. Puede utilizar un bus de eventos como Otto para ayudar a proporcionar dependencias.

EDIT: el siguiente en la forma de post http://systemdotrun.blogspot.co.uk/2014/11/android-testing-with-dagger-retrofit.html

Para probar una Activity usando Espresso + Dagger he hecho lo siguiente

Inspirado en la respuesta de @ user3399328 Tengo una clase DaggerHelper dentro de mi clase Application, lo que permite al caso de prueba anular el @Provider s usando Test @Modules que proporcionan @Modules . Mientras

1) Esto se hace antes de que se realice la llamada getActivity() del testCases (ya que mi llamada de inyección ocurre en mi actividad dentro de Activity.onCreate )

2) tearDown elimina los módulos de prueba del gráfico de objetos.

Ejemplos a continuación.

Nota: esto no es ideal ya que esto está sujeto a trampas similares de usar métodos de fábrica para IoC, pero al menos de esta manera es su única vez una sola llamada en tearDown () para llevar el sistema a prueba a la normalidad.

El DaggerHelper dentro de mi clase de Application

 public static class DaggerHelper { private static ObjectGraph sObjectGraph; private static final List<Object> productionModules; static { productionModules = new ArrayList<Object>(); productionModules.add(new DefaultModule()); } /** * Init the dagger object graph with production modules */ public static void initProductionModules() { initWithModules(productionModules); } /** * If passing in test modules make sure to override = true in the @Module annotation */ public static void initWithTestModules(Object... testModules) { initWithModules(getModulesAsList(testModules)); } private static void initWithModules(List<Object> modules) { sObjectGraph = ObjectGraph.create(modules.toArray()); } private static List<Object> getModulesAsList(Object... extraModules) { List<Object> allModules = new ArrayList<Object>(); allModules.addAll(productionModules); allModules.addAll(Arrays.asList(extraModules)); return allModules; } /** * Dagger convenience method - will inject the fields of the passed in object */ public static void inject(Object object) { sObjectGraph.inject(object); } } 

Mi módulo de prueba dentro de mi clase de prueba

 @Module ( overrides = true, injects = ActivityUnderTest.class ) static class TestDataPersisterModule { @Provides @Singleton DataPersister provideMockDataPersister() { return new DataPersister(){ @Override public void persistDose() { throw new RuntimeException("Mock DI!"); //just a test to see if being called } }; } } 

Método de prueba

 public void testSomething() { MyApp.DaggerHelper.initWithTestModules(new TestDataPersisterModule()); getActivity(); ... } 

Demoler

 @Override public void tearDown() throws Exception { super.tearDown(); //reset MyApp.DaggerHelper.initProductionModules(); } 
  • ¿Cómo inyectar una dependencia al probar una actividad de Android sin un marco de terceros?
  • Manejo del componente de la daga en el cambio de orientación
  • Inyección de Butterknife View
  • Uso de Dagger 2 para inyectar en servicio
  • Cómo inyectar el contexto usando RoboGuice en Android?
  • Dagger apoya la inyección de dependencia para las pruebas de ActivityInstrumentationTestCase2
  • Dagger 2 inyecta Android Context
  • Inyectar objetos de objetos en Class Util (usando Dagger)
  • InjectView en RoboFragment
  • Aplicación para Android con RoboGuice 2.0 - Cómo inyectar un singleton con el contexto de la aplicación
  • ¿Cómo inyectar la dependencia al fragmento androide anidado?
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.