android - para - dagger component
¿Es esta una forma correcta de usar la aplicación Dagger 2 para Android en la prueba de unidad para anular las dependencias con simulacros/falsificaciones? (2)
Para el proyecto Java ''regular'', es fácil anular las dependencias en las pruebas de unidad con las simuladas / falsas. Simplemente tiene que construir su componente Dagger y dárselo a la clase ''principal'' que impulsa su aplicación.
Para Android , las cosas no son tan simples y he buscado por mucho tiempo un ejemplo decente, pero no pude encontrarlo, así que tuve que crear mi propia implementación y realmente apreciaré los comentarios. ¿Es esta una forma correcta de usar Dagger 2 o no? es una forma más sencilla / elegante de anular las dependencias.
Aquí la explicación (la fuente del proyecto se puede encontrar en github ):
Dado que tenemos una aplicación simple que usa Dagger 2 con un solo componente de daga con un solo módulo, queremos crear pruebas unitarias de Android que usen JUnit4, Mockito y Espresso :
En la clase de Application
MyApp
, el componente / inyector se inicializa así:
public class MyApp extends Application {
private MyDaggerComponent mInjector;
public void onCreate() {
super.onCreate();
initInjector();
}
protected void initInjector() {
mInjector = DaggerMyDaggerComponent.builder().httpModule(new HttpModule(new OkHttpClient())).build();
onInjectorInitialized(mInjector);
}
private void onInjectorInitialized(MyDaggerComponent inj) {
inj.inject(this);
}
public void externalInjectorInitialization(MyDaggerComponent injector) {
mInjector = injector;
onInjectorInitialized(injector);
}
...
En el código anterior: el inicio normal de la aplicación se realiza a través de onCreate()
que llama a initInjector()
que crea el inyector y luego llama a onInjectorInitialized()
.
El método externalInjectorInitialization()
debe ser llamado por las pruebas unitarias para set
el inyector desde una fuente externa, es decir, una prueba unitaria.
Hasta ahora tan bueno.
Veamos cómo se ven las cosas en las pruebas unitarias:
Necesitamos crear llamadas MyTestApp que extiendan la clase MyApp y anulen a initInjector
con un método vacío para evitar la creación de doble inyector (porque crearemos uno nuevo en nuestra prueba de unidad):
public class MyTestApp extends MyApp {
@Override
protected void initInjector() {
// empty
}
}
Entonces tenemos que reemplazar de alguna manera la MyApp original con MyTestApp. Esto se hace a través de un corredor de prueba personalizado:
public class MyTestRunner extends AndroidJUnitRunner {
@Override
public Application newApplication(ClassLoader cl,
String className,
Context context) throws InstantiationException,
IllegalAccessException,
ClassNotFoundException {
return super.newApplication(cl, MyTestApp.class.getName(), context);
}
}
... donde en newApplication()
reemplazamos efectivamente la clase de aplicación original con la de prueba.
Luego tenemos que decirle al marco de prueba que tenemos y queremos usar nuestro corredor de prueba personalizado, por lo que en el build.gradle agregamos:
defaultConfig {
...
testInstrumentationRunner ''com.bolyartech.d2overrides.utils.MyTestRunner''
...
}
Cuando se ejecuta una prueba de unidad, mi MyApp
original se reemplaza con MyTestApp
. Ahora tenemos que crear y proporcionar nuestro componente / inyector con simulacros / falsificaciones a la aplicación con externalInjectorInitialization()
. Para ello extendemos la ActivityTestRule normal:
@Rule
public ActivityTestRule<Act_Main> mActivityRule = new ActivityTestRule<Act_Main>(
Act_Main.class) {
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
OkHttpClient mockHttp = create mock OkHttpClient
MyDaggerComponent injector = DaggerMyDaggerComponent.
builder().httpModule(new HttpModule(mockHttp)).build();
MyApp app = (MyApp) InstrumentationRegistry.getInstrumentation().
getTargetContext().getApplicationContext();
app.externalInjectorInitialization(injector);
}
};
Y luego hacemos nuestra prueba de la manera habitual.
@Test
public void testHttpRequest() throws IOException {
onView(withId(R.id.btn_execute)).perform(click());
onView(withId(R.id.tv_result))
.check(matches(withText(EXPECTED_RESPONSE_BODY)));
}
El método anterior para las modificaciones de (módulo) funciona, pero requiere la creación de una clase de prueba por cada prueba para poder proporcionar una regla / (configuración de simulacros) por cada prueba. Sospecho / adivino / espero que haya una forma más fácil y elegante. ¿Esta ahí?
Este método se basa en gran medida en la respuesta de @tomrozb para esta pregunta . Acabo de agregar la lógica para evitar la creación de doble inyector.
1. Inyectar sobre dependencias.
Dos cosas a tener en cuenta:
- Los componentes pueden proveerse
- Si puede inyectarlo una vez, puede inyectarlo nuevamente (y anular las dependencias anteriores)
Lo que hago es inyectar desde mi caso de prueba sobre las dependencias antiguas. Dado que su código está limpio y todo está correctamente incluido, nada debería salir mal, ¿verdad?
Lo siguiente solo funcionará si no confía en Global State, ya que cambiar el componente de la aplicación en tiempo de ejecución no funcionará si mantiene las referencias al anterior en algún lugar. Tan pronto como cree su próxima Activity
se buscará el nuevo componente de la aplicación y se proporcionarán sus dependencias de prueba.
Este método depende de la correcta manipulación de los ámbitos. Terminar y reiniciar una actividad debe recrear sus dependencias. Por lo tanto, puede cambiar los componentes de la aplicación cuando no hay actividad en ejecución o antes de iniciar una nueva.
En tu testcase solo crea tu componente como lo necesites
// in @Test or @Before, just inject ''over'' the old state
App app = (App) InstrumentationRegistry.getTargetContext().getApplicationContext();
AppComponent component = DaggerAppComponent.builder()
.appModule(new AppModule(app))
.build();
component.inject(app);
Si tienes una aplicación como la siguiente ...
public class App extends Application {
@Inject
AppComponent mComponent;
@Override
public void onCreate() {
super.onCreate();
DaggerAppComponent.builder().appModule(new AppModule(this)).build().inject(this);
}
}
... se inyectará a sí mismo y a cualquier otra dependencia que haya definido en su Application
. Cualquier llamada posterior obtendrá las nuevas dependencias.
2. Usa una configuración y aplicación diferente
Puede elegir la configuración que se utilizará con su prueba de instrumentación:
android {
...
testBuildType "staging"
}
El uso de la combinación de recursos de Gradle le permite utilizar varias versiones diferentes de su App
para diferentes tipos de compilación.
Mueva su clase de Application
de la carpeta de origen main
carpetas de debug
y release
. Gradle compilará el conjunto de fuente correcto dependiendo de la configuración. Luego puede modificar su versión de depuración y lanzamiento de su aplicación según sus necesidades.
Si no desea tener diferentes clases de Application
para depuración y lanzamiento, podría crear otro tipo de buildType
, que se utilice solo para las pruebas de instrumentación. Se aplica el mismo principio: duplique la clase de Application
en cada carpeta de conjunto de fuentes, o recibirá errores de compilación. Como entonces necesitaría tener la misma clase en el directorio debug
y rlease
, puede hacer que otro directorio contenga su clase utilizada tanto para la depuración como para la versión. Luego, agregue el directorio usado a sus conjuntos de fuentes de depuración y lanzamiento.
Hay una forma más sencilla de hacerlo, incluso los documentos de Dagger 2 lo mencionan, pero no lo hacen muy obvio. Aquí hay un fragmento de la documentación.
@Provides static Pump providePump(Thermosiphon pump) {
return pump;
}
El termosifón implementa la bomba y dondequiera que se solicite una bomba, Dagger inyecta un termosifón.
Volviendo a su ejemplo. Puede crear un módulo con un miembro de datos booleanos estáticos que le permite cambiar dinámicamente entre sus objetos de prueba reales y simulados, como así.
@Module
public class HttpModule {
private static boolean isMockingHttp;
public HttpModule() {}
public static boolean mockHttp(boolean isMockingHttp) {
HttpModule.isMockingHttp = isMockingHttp;
}
@Provides
HttpClient providesHttpClient(OkHttpClient impl, MockHttpClient mockImpl) {
return HttpModule.isMockingHttp ? mockImpl : impl;
}
}
HttpClient puede ser la super clase que se extiende o una interfaz implementada por OkHttpClient y MockHttpClient. Dagger construirá automáticamente la clase requerida e inyectará sus dependencias internas al igual que Thermosiphon.
Para burlarse de su HttpClient, simplemente llame a HttpModule.mockHttp(true)
antes de que sus dependencias se inyecten en el código de su aplicación.
Los beneficios de este enfoque son:
- No es necesario crear componentes de prueba separados, ya que las simulaciones se inyectan a nivel de módulo.
- El código de la aplicación permanece prístino.