implement - dagger android support
¿Cómo anular un módulo/dependencia en una prueba de unidad con Dagger 2.0? (8)
¿Pueden ver mi solución? He incluido un 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:
Creo una clase para crear 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); ... } }
Tengo un TestApplication personalizado que extiende 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; } }
Finalmente, intentaré burlarme o rechazar la dependencia del módulo y el generador para proporcionar una dependencia falsa a la actividad:
@MediumTest @RunWith(AndroidJUnit4.class) public class PurchaseActivityTest { @Rule public ActivityTestRule<PurchaseActivity> 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()); }
Tengo una actividad simple de Android con una sola dependencia. Yo inyecto la dependencia en el onCreate
la actividad de esta manera:
Dagger_HelloComponent.builder()
.helloModule(new HelloModule(this))
.build()
.initialize(this);
En mi ActivityUnitTestCase
quiero anular la dependencia con un simulacro de Mockito. Supongo que necesito usar un módulo específico de prueba que proporciona el simulacro, pero no puedo averiguar cómo 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 equivalente de Dagger 2.0 de lo anterior?
Puedes ver mi proyecto y su prueba de unidad aquí en GitHub .
Como bien dice @EpicPandaForce, no se pueden extender los módulos. Sin embargo, se me ocurrió una solución astuta para esto que creo que evita muchos de los problemas que sufren los otros ejemplos.
El truco para "extender" un Módulo es crear una simulación parcial y simular los métodos del proveedor que desea anular.
Utilizando Mockito :
MyModule module = Mockito.spy(new MyModule());
Mockito.doReturn("mocked string").when(module).provideString();
MyComponent component = DaggerMyComponent.builder()
.myModule(module)
.build();
app.setComponent(component);
He creado esta esencia aquí para mostrar un ejemplo completo.
EDITAR
Resulta que puedes hacer esto incluso sin una simulación parcial, así:
MyComponent component = DaggerMyComponent.builder()
.myModule(new MyModule() {
@Override public String provideString() {
return "mocked string";
}
})
.build();
app.setComponent(component);
Con Dagger2, puede pasar un módulo específico (el TestModule allí) a un componente utilizando la api generadora generada.
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.
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
PRODUCTION. Pude hacer que esto funcionara de forma ligeramente diferente, por lo que mi aplicación de producción no tiene que saber nada sobre mi entorno de prueba.
TL; DR: amplíe su clase de aplicación con una aplicación de prueba que use su módulo 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 (normalmente marcadas con @Singleton
). Si su aplicación tiene componentes con un alcance diferente (por ejemplo, por actividad), entonces deberá crear subclases para cada alcance o utilizar la respuesta original de @ tomrozb. Gracias a @tomrozb por señalar esto!
Este ejemplo utiliza el AndroidJUnitRunner prueba AndroidJUnitRunner , pero probablemente se pueda adaptar 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 manera, mis actividades y otras clases que utilizan @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, extienda 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 tu burla y esas cosas aquí (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.
}
Así que 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 configurar 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 sus 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 que falta es anular su TestRunner con un corredor de prueba personalizado. En mi proyecto estaba usando el AndroidJUnitRunner
pero parece que puedes 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 tendrás que actualizar tu testInstrumentationRunner gradle, así:
testInstrumentationRunner "com.mypackage.TestRunner"
Y si está utilizando Android Studio, también tendrá que hacer clic en Editar configuración en el menú Ejecutar e ingresar el nombre de su corredor de prueba en "Corredor de instrumentación específico".
¡Y eso es! Esperemos 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í:
MyComponent.java
interface MyComponent {
Foo provideFoo();
}
Luego tenemos dos módulos diferentes: uno real y uno de prueba.
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 aplicación hacemos esto:
MyComponent component = DaggerMyRealComponent.create();
<...>
Foo foo = component.getFoo();
Mientras que 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 al tener MyModule dentro de TestModule y usar los métodos de MyModule a menos que estén configurados 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;
}
}
Probablemente esta es una solución más que la compatibilidad adecuada para anular el módulo de prueba, pero permite anular los módulos de producción con la prueba uno. Los fragmentos de código a continuación muestran un caso simple cuando solo tiene un componente y un módulo, pero esto debería funcionar para cualquier escenario. Requiere una gran cantidad de repetición y código de repetición, así que ten en cuenta esto. Estoy seguro de que habrá una mejor manera de lograr esto en el futuro.
También he creado 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
- 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 "Liberar cadena". 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 a StringHolder
"Cadena de prueba". Debemos configurar el componente de prueba en la clase de App
antes de MainActivity
, 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 configurar el componente de prueba utilizando el método de establecimiento 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 crear la actividad. Puede encontrar el código para DaggerActivityTestRule
here .
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<MainActivity> mActivityRule =
new DaggerActivityTestRule<>(MainActivity.class, new OnBeforeActivityLaunchedListener<MainActivity>() {
@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(...)
}
}
Tengo solución para Roboletric 3. + .
Tengo MainActivity que quiero probar sin inyección al crear:
public class MainActivity extends BaseActivity{
@Inject
public Configuration configuration;
@Inject
public AppStateService appStateService;
@Inject
public LoginService loginService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.processIntent(getIntent()); // this is point where pass info from test
super.onCreate(savedInstanceState)
...
}
...
}
Siguiente mi BaseActivty:
public class BaseActivity extends AppCompatActivity {
protected Logger mLog;
protected boolean isTestingSession = false; //info about test session
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (!isTestingSession) { // check if it is in test session, if not enable injectig
AndroidInjection.inject(this);
}
super.onCreate(savedInstanceState);
}
// method for receive intent from child and scaning if has item TESTING with true
protected void processIntent(Intent intent) {
if (intent != null && intent.getExtras() != null) {
isTestingSession = intent.getExtras().getBoolean("TESTING", false);
}
}
finalmente mi clase de prueba
@Before
public void setUp() throws Exception {
...
// init mocks...
loginServiceMock = mock(LoginService.class);
locServiceMock = mock(LocationClientService.class);
fakeConfiguration = new ConfigurationUtils(new ConfigurationXmlParser());
fakeConfiguration.save(FAKE_XML_CONFIGURATION);
appStateService = new AppStateService(fakeConfiguration, locServiceMock, RuntimeEnvironment.application);
// prepare activity
Intent intent = new Intent(RuntimeEnvironment.application, MainActivity.class);
intent.putExtra("TESTING", true);
ActivityController<MainActivity> activityController = Robolectric.buildActivity(MainActivity.class, intent); // place to put bundle with extras
// get the activity instance
mainActivity = activityController.get();
// init fields which should be injected
mainActivity.appStateService = appStateService;
mainActivity.loginService = loginServiceMock;
mainActivity.configuration = fakeConfiguration;
// and whoala
// now setup your activity after mock injection
activityController.setup();
// get views etc..
actionButton = mainActivity.findViewById(R.id.mainButtonAction);
NavigationView navigationView = mainActivity.findViewById(R.id.nav_view);
....
}
ESTA RESPUESTA ES OBSOLETA. LEA ABAJO EN EDITAR.
Suficientemente decepcionante, no puede extenderse desde un módulo , o obtendrá el siguiente error de compilación:
Error:(24, 21) error: @Provides methods may not override another method.
Overrides: Provides
retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.myServerEndpoint()
Lo que significa que no puede simplemente extender un "módulo simulado" y reemplazar su módulo original. No, no es tan fácil. Y considerando que diseñas tus componentes de tal manera que une directamente los módulos por clase, tampoco puedes simplemente hacer un "TestComponent", porque eso significaría que tienes que reinventar todo desde cero, y tendrías ¡Para hacer un componente para cada variación! Claramente eso no es una opción.
Así que en la escala más pequeña, lo que terminé haciendo es hacer un "proveedor" que le doy al módulo, lo que determina si selecciono el tipo de producción o la simulació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 reemplazar 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.