¿Cómo se anula un módulo / dependencia en una prueba unitaria con Dagger 2.0?

Tengo una actividad simple de Android con una sola dependencia. onCreate la dependencia en la actividad onCreate así:

 Dagger_HelloComponent.builder() .helloModule(new HelloModule(this)) .build() .initialize(this); 

En mi ActivityUnitTestCase , deseo anular la dependencia con un simulacro de Mockito. Supongo que necesito usar un módulo específico de prueba que proporcione el simulacro, pero no puedo encontrar la manera de agregar este módulo al gráfico de objetos.

En Dagger 1.x esto aparentemente se hace con algo como esto :

 @Before public void setUp() { ObjectGraph.create(new TestModule()).inject(this); } 

¿Cuál es el Dagger 2.0 equivalente al anterior?

Puedes ver mi proyecto y su prueba de unidad aquí en GitHub .

Probablemente esto sea más una solución alternativa que el soporte adecuado para la anulación de los módulos de prueba, pero permite anular los módulos de producción con el de prueba. Los fragmentos de código a continuación muestran un caso simple cuando tiene solo un componente y un módulo, pero esto debería funcionar para cualquier escenario. Requiere mucha repetición repetitiva y de código, así que tenga en cuenta esto. Estoy seguro de que habrá una mejor manera de lograr esto en el futuro.

También creé un proyecto con ejemplos para Espresso y Robolectric . Esta respuesta se basa en el código contenido en el proyecto.

La solución requiere dos cosas:

  • proporcionar setter adicional para @Component
  • el componente de prueba debe extender el componente de producción

Supongamos que tenemos una Application simple como la siguiente:

 public class App extends Application { private AppComponent mAppComponent; @Override public void onCreate() { super.onCreate(); mAppComponent = DaggerApp_AppComponent.create(); } public AppComponent component() { return mAppComponent; } @Singleton @Component(modules = StringHolderModule.class) public interface AppComponent { void inject(MainActivity activity); } @Module public static class StringHolderModule { @Provides StringHolder provideString() { return new StringHolder("Release string"); } } } 

Tenemos que agregar un método adicional a la clase de la App . Esto nos permite reemplazar el componente de producción.

 /** * Visible only for testing purposes. */ // @VisibleForTesting public void setTestComponent(AppComponent appComponent) { mAppComponent = appComponent; } 

Como puede ver, el objeto StringHolder contiene el valor “Release string”. Este objeto se inyecta a la MainActivity .

 public class MainActivity extends ActionBarActivity { @Inject StringHolder mStringHolder; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((App) getApplication()).component().inject(this); } } 

En nuestras pruebas, queremos proporcionar StringHolder con “Cadena de prueba”. Tenemos que establecer el componente de prueba en la clase de la App antes de que se cree la actividad principal, porque StringHolder se inyecta en la onCreate llamada onCreate .

En Dagger v2.0.0 los componentes pueden extender otras interfaces. Podemos aprovechar esto para crear nuestro TestAppComponent que extiende AppComponent .

 @Component(modules = TestStringHolderModule.class) interface TestAppComponent extends AppComponent { } 

Ahora podemos definir nuestros módulos de prueba, por ejemplo, TestStringHolderModule . El último paso es establecer el componente de prueba utilizando el método setter agregado previamente en la clase de la App . Es importante hacer esto antes de crear la actividad.

 ((App) application).setTestComponent(mTestAppComponent); 

Café exprés

Para Espresso, he creado ActivityTestRule personalizado que permite intercambiar el componente antes de que se cree la actividad. Puede encontrar el código para DaggerActivityTestRule aquí .

Prueba de muestra con Espresso:

 @RunWith(AndroidJUnit4.class) @LargeTest public class MainActivityEspressoTest { public static final String TEST_STRING = "Test string"; private TestAppComponent mTestAppComponent; @Rule public ActivityTestRule mActivityRule = new DaggerActivityTestRule<>(MainActivity.class, new OnBeforeActivityLaunchedListener() { @Override public void beforeActivityLaunched(@NonNull Application application, @NonNull MainActivity activity) { mTestAppComponent = DaggerMainActivityEspressoTest_TestAppComponent.create(); ((App) application).setTestComponent(mTestAppComponent); } }); @Component(modules = TestStringHolderModule.class) interface TestAppComponent extends AppComponent { } @Module static class TestStringHolderModule { @Provides StringHolder provideString() { return new StringHolder(TEST_STRING); } } @Test public void checkSomething() { // given ... // when onView(...) // then onView(...) .check(...); } } 

Robolectric

Es mucho más fácil con Robolectric gracias a RuntimeEnvironment.application .

Prueba de muestra con Robolectric:

 @RunWith(RobolectricGradleTestRunner.class) @Config(emulateSdk = 21, reportSdk = 21, constants = BuildConfig.class) public class MainActivityRobolectricTest { public static final String TEST_STRING = "Test string"; @Before public void setTestComponent() { AppComponent appComponent = DaggerMainActivityRobolectricTest_TestAppComponent.create(); ((App) RuntimeEnvironment.application).setTestComponent(appComponent); } @Component(modules = TestStringHolderModule.class) interface TestAppComponent extends AppComponent { } @Module static class TestStringHolderModule { @Provides StringHolder provideString() { return new StringHolder(TEST_STRING); } } @Test public void checkSomething() { // given MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class); // when ... // then assertThat(...) } } 

Como dice correctamente @EpicPandaForce, no puede ampliar los Módulos. Sin embargo, se me ocurrió una solución furtiva para esto, que creo que evita muchas de las repeticiones que sufren los otros ejemplos.

El truco para “extender” un Módulo es crear un simulacro parcial y simular los métodos del proveedor que desea anular.

Usando Mockito :

 MyModule module = Mockito.spy(new MyModule()); Mockito.doReturn("mocked string").when(module).provideString(); MyComponent component = DaggerMyComponent.builder() .myModule(module) .build(); app.setComponent(component); 

Creé esta esencia aquí para mostrar un ejemplo completo.

EDITAR

Resulta que puedes hacer esto incluso sin un simulacro parcial, así:

 MyComponent component = DaggerMyComponent.builder() .myModule(new MyModule() { @Override public String provideString() { return "mocked string"; } }) .build(); app.setComponent(component); 

La solución propuesta por @tomrozb es muy buena y me puso en el camino correcto, pero mi problema fue que expuso un método setTestComponent() en la clase de Application PRODUCCIÓN. Pude hacer que funcionara de manera diferente, de modo que mi aplicación de producción no tiene que saber absolutamente nada sobre mi entorno de prueba.

TL; DR: amplíe su clase de aplicación con una aplicación de prueba que use su componente y componente de prueba. Luego, cree un corredor de prueba personalizado que se ejecute en la aplicación de prueba en lugar de su aplicación de producción.


EDITAR: Este método solo funciona para dependencias globales (generalmente marcadas con @Singleton ). Si su aplicación tiene componentes con un scope diferente (por ejemplo, por actividad), entonces deberá crear subclases para cada ámbito o utilizar la respuesta original de @ tomrozb. ¡Gracias a @tomrozb por señalar esto!


Este ejemplo utiliza el corrector de prueba AndroidJUnitRunner , pero probablemente podría adaptarse a Robolectric y otros.

Primero, mi aplicación de producción. Se ve algo como esto:

 public class MyApp extends Application { protected MyComponent component; public void setComponent() { component = DaggerMyComponent.builder() .myModule(new MyModule()) .build(); component.inject(this); } public MyComponent getComponent() { return component; } @Override public void onCreate() { super.onCreate(); setComponent(); } } 

De esta forma, mis actividades y otras clases que usan @Inject simplemente tienen que llamar a algo como getApp().getComponent().inject(this); para inyectarse en el gráfico de dependencia.

Para completar, aquí está mi componente:

 @Singleton @Component(modules = {MyModule.class}) public interface MyComponent { void inject(MyApp app); // other injects and getters } 

Y mi módulo:

 @Module public class MyModule { // EDIT: This solution only works for global dependencies @Provides @Singleton public MyClass provideMyClass() { ... } // ... other providers } 

Para el entorno de prueba, amplíe su componente de prueba desde su componente de producción. Esto es lo mismo que en la respuesta de @ tomrozb.

 @Singleton @Component(modules = {MyTestModule.class}) public interface MyTestComponent extends MyComponent { // more component methods if necessary } 

Y el módulo de prueba puede ser lo que quieras. Presumiblemente manejarás tus burlas y cosas aquí (yo uso Mockito).

 @Module public class MyTestModule { // EDIT: This solution only works for global dependencies @Provides @Singleton public MyClass provideMyClass() { ... } // Make sure to implement all the same methods here that are in MyModule, // even though it's not an override. } 

Entonces ahora, la parte difícil. Cree una clase de aplicación de prueba que se extienda desde su clase de aplicación de producción y anule el método setComponent() para establecer el componente de prueba con el módulo de prueba. Tenga en cuenta que esto solo puede funcionar si MyTestComponent es un descendiente de MyComponent .

 public class MyTestApp extends MyApp { // Make sure to call this method during setup of your tests! @Override public void setComponent() { component = DaggerMyTestComponent.builder() .myTestModule(new MyTestModule()) .build(); component.inject(this) } } 

Asegúrese de llamar a setComponent() en la aplicación antes de comenzar las pruebas para asegurarse de que el gráfico esté configurado correctamente. Algo como esto:

 @Before public void setUp() { MyTestApp app = (MyTestApp) getInstrumentation().getTargetContext().getApplicationContext(); app.setComponent() ((MyTestComponent) app.getComponent()).inject(this) } 

Finalmente, la última pieza faltante es anular tu TestRunner con un corredor de prueba personalizado. En mi proyecto estaba usando AndroidJUnitRunner pero parece que puedes hacer lo mismo con Robolectric .

 public class TestRunner extends AndroidJUnitRunner { @Override public Application newApplication(@NonNull ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return super.newApplication(cl, MyTestApp.class.getName(), context); } } 

También deberá actualizar su testInstrumentationRunner gradle, así:

 testInstrumentationRunner "com.mypackage.TestRunner" 

Y si está usando Android Studio, también deberá hacer clic en Editar configuración en el menú de ejecución e ingresar el nombre de su corredor de prueba en “Corredor de instrumentación específico”.

¡Y eso es! Espero que esta información ayude a alguien 🙂

Parece que he encontrado otra manera y está funcionando hasta ahora.

Primero, una interfaz de componente que no es un componente en sí mismo:

MyComponent.java

 interface MyComponent { Foo provideFoo(); } 

Entonces tenemos dos módulos diferentes: uno real y prueba uno.

MyModule.java

 @Module class MyModule { @Provides public Foo getFoo() { return new Foo(); } } 

TestModule.java

 @Module class TestModule { private Foo foo; public void setFoo(Foo foo) { this.foo = foo; } @Provides public Foo getFoo() { return foo; } } 

Y tenemos dos componentes para usar estos dos módulos:

MyRealComponent.java

 @Component(modules=MyModule.class) interface MyRealComponent extends MyComponent { Foo provideFoo(); // without this dagger will not do its magic } 

MyTestComponent.java

 @Component(modules=TestModule.class) interface MyTestComponent extends MyComponent { Foo provideFoo(); } 

En la aplicación hacemos esto:

 MyComponent component = DaggerMyRealComponent.create(); <...> Foo foo = component.getFoo(); 

Mientras estamos en el código de prueba, usamos:

 TestModule testModule = new TestModule(); testModule.setFoo(someMockFoo); MyComponent component = DaggerMyTestComponent.builder() .testModule(testModule).build(); <...> Foo foo = component.getFoo(); // will return someMockFoo 

El problema es que tenemos que copiar todos los métodos de MyModule en TestModule, pero se puede hacer teniendo MyModule dentro de TestModule y usando los métodos de MyModule a menos que se configuren directamente desde afuera. Me gusta esto:

TestModule.java

 @Module class TestModule { MyModule myModule = new MyModule(); private Foo foo = myModule.getFoo(); public void setFoo(Foo foo) { this.foo = foo; } @Provides public Foo getFoo() { return foo; } } 

ESTA RESPUESTA ES OBSOLETA. LEA A CONTINUACIÓN EN EDITAR.

Desafortunadamente, no puedes extender desde un Módulo , o obtendrás el siguiente error de comstackción:

 Error:(24, 21) error: @Provides methods may not override another method. Overrides: Provides retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.mySe‌​rverEndpoint() 

Lo que significa que no puedes simplemente extender un “módulo simulado” y reemplazar tu módulo original. No, no es tan fácil. Y teniendo en cuenta que diseñe sus componentes de manera que vincule directamente los módulos por clase, tampoco puede hacer un “componente de prueba”, porque eso significa que debe reinventar todo desde cero, y tendría que para hacer un componente para cada variación! Claramente, esa no es una opción.

Entonces, en una escala más pequeña, lo que terminé haciendo es crear un “proveedor” que entrego al módulo, que determina si selecciono el simulacro o el tipo de producción.

 public interface EndpointProvider { Endpoint serverEndpoint(); } public class ProdEndpointProvider implements EndpointProvider { @Override public Endpoint serverEndpoint() { return new ServerEndpoint(); } } public class TestEndpointProvider implements EndpointProvider { @Override public Endpoint serverEndpoint() { return new TestServerEndpoint(); } } @Module public class EndpointModule { private Endpoint serverEndpoint; private EndpointProvider endpointProvider; public EndpointModule(EndpointProvider endpointProvider) { this.endpointProvider = endpointProvider; } @Named("server") @Provides public Endpoint serverEndpoint() { return endpointProvider.serverEndpoint(); } } 

EDITAR: Aparentemente, como dice el mensaje de error, NO PUEDE anular otro método utilizando un método anotado @Provides , pero eso no significa que no pueda anular un método anotado @Provides 🙁

¡Toda esa magia fue en vano! Puede extender un Módulo sin poner @Provides en el método y funciona … Consulte la respuesta de @vaughandroid.

Pueden ver mi solución, he incluido el ejemplo de subcomponente: https://github.com/nongdenchet/android-mvvm-with-tests . Gracias @vaughandroid, he tomado prestados tus métodos principales. Aquí está el punto principal:

  1. Creo una clase para crear un subcomponente. Mi aplicación personalizada también tendrá una instancia de esta clase:

     // The builder class public class ComponentBuilder { private AppComponent appComponent; public ComponentBuilder(AppComponent appComponent) { this.appComponent = appComponent; } public PlacesComponent placesComponent() { return appComponent.plus(new PlacesModule()); } public PurchaseComponent purchaseComponent() { return appComponent.plus(new PurchaseModule()); } } // My custom application class public class MyApplication extends Application { protected AppComponent mAppComponent; protected ComponentBuilder mComponentBuilder; @Override public void onCreate() { super.onCreate(); // Create app component mAppComponent = DaggerAppComponent.builder() .appModule(new AppModule()) .build(); // Create component builder mComponentBuilder = new ComponentBuilder(mAppComponent); } public AppComponent component() { return mAppComponent; } public ComponentBuilder builder() { return mComponentBuilder; } } // Sample using builder class: public class PurchaseActivity extends BaseActivity { ... @Override protected void onCreate(Bundle savedInstanceState) { ... // Setup dependency ((MyApplication) getApplication()) .builder() .purchaseComponent() .inject(this); ... } } 
  2. Tengo una TestApplication personalizada que amplía la clase MyApplication anterior. Esta clase contiene dos métodos para reemplazar el componente raíz y el constructor:

     public class TestApplication extends MyApplication { public void setComponent(AppComponent appComponent) { this.mAppComponent = appComponent; } public void setComponentBuilder(ComponentBuilder componentBuilder) { this.mComponentBuilder = componentBuilder; } } 
  3. Finalmente, intentaré simular o rescindir la dependencia del módulo y del generador para proporcionar una dependencia falsa a la actividad:

     @MediumTest @RunWith(AndroidJUnit4.class) public class PurchaseActivityTest { @Rule public ActivityTestRule activityTestRule = new ActivityTestRule<>(PurchaseActivity.class, true, false); @Before public void setUp() throws Exception { PurchaseModule stubModule = new PurchaseModule() { @Provides @ViewScope public IPurchaseViewModel providePurchaseViewModel(IPurchaseApi purchaseApi) { return new StubPurchaseViewModel(); } }; // Setup test component AppComponent component = ApplicationUtils.application().component(); ApplicationUtils.application().setComponentBuilder(new ComponentBuilder(component) { @Override public PurchaseComponent purchaseComponent() { return component.plus(stubModule); } }); // Run the activity activityTestRule.launchActivity(new Intent()); } 

Con Dagger2, puede pasar un módulo específico (el TestModule allí) a un componente utilizando la API generada del generador.

 ApplicationComponent appComponent = Dagger_ApplicationComponent.builder() .helloModule(new TestModule()) .build(); 

Tenga en cuenta que Dagger_ApplicationComponent es una clase generada con la nueva anotación @Component.