android dependency-injection functional-testing dagger android-espresso

haciendo que Dagger inyecte objetos simulados al hacer una prueba funcional de espresso para Android



dependency-injection functional-testing (4)

Recientemente me he vuelto loco con Dagger porque el concepto de DI tiene mucho sentido. Uno de los "subproductos" más agradables de DI (como lo puso Jake Wharton en una de sus presentaciones) es la verificación más fácil.

Así que ahora estoy básicamente usando espresso para hacer algunas pruebas funcionales, y quiero poder inyectar datos ficticios / simulados en la aplicación y hacer que la actividad se muestre. Supongo que desde entonces, esta es una de las mayores ventajas de DI, esta debería ser una pregunta relativamente simple. Por alguna razón, sin embargo, parece que no puedo envolver mi cabeza alrededor de ella. 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 simulado 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 se ignora por completo ... arr ... :(

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

EDITAR:

Así que opté por el enfoque de @ user3399328 de tener una definición de lista de módulos de prueba estática, verificando el valor nulo y luego agregarlo a mi clase de Aplicación. Sin embargo, todavía no estoy recibiendo mi versión de prueba inyectada de la clase. Sin embargo, tengo la sensación de que probablemente haya algún problema con la definición del módulo de prueba de daga y no con mi ciclo de vida de espresso. La razón por la que hago el supuesto es que agrego sentencias de depuración y encuentro que el módulo de prueba estático no está vacío en el momento de la inyección en la clase de aplicación. ¿Podría indicarme una dirección de lo que podría estar haciendo mal? Aquí están los 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; } }


Con Dagger 2 y Espresso 2 las cosas han mejorado. Así es como podría verse un caso de prueba ahora. Tenga en cuenta que ContributorsModel es proporcionado por Dagger. La demostración 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")))); }


EDITAR: el siguiente en 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 de aplicación, que permite que el caso de prueba anule los @Provider s usando Test @Modules que proporcionan @Modules . Mientras

1) Esto se hace antes de que se realice la getActivity() testCases getActivity() (ya que mi llamada a la 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 está sujeto a dificultades similares en el uso de los métodos de fábrica para IoC, pero al menos de esta manera es solo una única llamada en tearDown () para que el sistema a prueba vuelva 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(); }


La llamada a getActivity realmente iniciará su actividad invocando onCreate en el proceso, lo que significa que no se agregarán los módulos de prueba al gráfico a tiempo para ser utilizados. Al utilizar activityInstrumentationTestcase2, realmente no se puede inyectar correctamente en el ámbito de la actividad. He resuelto este problema utilizando mi aplicación para proporcionar dependencias a mis actividades y luego inyectar objetos simulados en ella que utilizarán las actividades. No es ideal pero funciona. Puede utilizar un bus de eventos como Otto para ayudar a proporcionar dependencias.


Su enfoque no funciona porque solo sucede una vez, y como Matt mencionó, cuando se ejecuta el código de inyección real de la actividad, eliminará todas las variables inyectadas por su gráfico de objetos especiales.

Hay dos maneras de hacer que esto funcione.

La forma rápida: haga una variable estática pública en su actividad para que una prueba pueda asignar un módulo de anulación y tenga el código de actividad real siempre incluya este módulo si no es nulo (lo que solo ocurrirá en las pruebas). Es similar a mi respuesta here solo para la clase base de su actividad en lugar de la aplicación.

La forma más larga, probablemente mejor: refactorice su código para que toda la inyección de actividad (y, lo que es más importante, la creación de gráficos) ocurra 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 que también incluye los módulos de prueba. Como las clases de prueba se cargan primero, su aplicación se ejecutará con el ActivityInjectHelper de prueba. Nuevamente es similar a mi respuesta here solo para una clase diferente.

ACTUALIZAR:

Veo que has publicado más código y está cerca de funcionar, pero no hay cigarros. Tanto para las actividades como para las aplicaciones, el módulo de prueba debe estar colgado antes de que se ejecute 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 ya se ha llamado a onCreate () cuando se ejecuta setUp (). Afortunadamente, hacerlo en el trabajo del constructor de la prueba, la aplicación no se ha creado en ese momento. Menciono brevemente esto en mi primer enlace.