J'essaie de configurer Dagger dans mes tests d'instrumentation Espresso afin de simuler des appels à des ressources externes (services RESTful dans ce cas). Le modèle que j'ai suivi dans Robolectric pour mes tests unitaires consistait à étendre ma classe d'applications de production et à remplacer les modules Dagger par des modules de test qui renverraient des exemples. J'essaie de faire la même chose ici, mais j'obtiens une exception ClassCastException dans mes tests Espresso lorsque j'essaie de convertir l'application vers mon application personnalisée.
Voici ma mise en place jusqu'à présent:
Production
Sous app/src/main/Java/com/mypackage/injection, j'ai:
MyCustomApplication
package com.mypackage.injection;
import Android.app.Application;
import Java.util.ArrayList;
import Java.util.List;
import dagger.ObjectGraph;
public class MyCustomApplication extends Application {
protected ObjectGraph graph;
@Override
public void onCreate() {
super.onCreate();
graph = ObjectGraph.create(getModules().toArray());
}
protected List<Object> getModules() {
List<Object> modules = new ArrayList<Object>();
modules.add(new AndroidModule(this));
modules.add(new RemoteResourcesModule(this));
modules.add(new MyCustomModule());
return modules;
}
public void inject(Object object) {
graph.inject(object);
}
}
Que j'utilise de la manière suivante:
BaseActivity
package com.mypackage.injection.views;
import Android.app.Activity;
import Android.os.Bundle;
import com.mypackage.injection.MyCustomApplication;
public abstract class MyCustomBaseActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((MyCustomApplication)getApplication()).inject(this);
}
}
Activité sous test
package com.mypackage.views.mydomain;
// imports snipped for bevity
public class MyActivity extends MyBaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//snip
}
}
Configuration Espresso
Sous app/src/androidTest/Java/com/mypackage/injection, j'ai:
MyCustomEspressoApplication
package com.mypackage.injection;
import Java.util.ArrayList;
import Java.util.List;
import dagger.ObjectGraph;
public class MyCustomEspressoApplication extends MyCustomApplication {
private AndroidModule androidModule;
private MyCustomModule myCustomModule;
private EspressoRemoteResourcesModule espressoRemoteResourcesModule;
@Override
public void onCreate() {
super.onCreate();
graph = ObjectGraph.create(getModules().toArray());
}
protected List<Object> getModules() {
List<Object> modules = new ArrayList<Object>();
modules.add(getAndroidModule());
modules.add(getEspressoRemoteResourcesModule());
modules.add(getMyCustomModule());
return modules;
}
public void inject(Object object) {
graph.inject(object);
}
public AndroidModule getAndroidModule() {
if (this.androidModule == null) {
this.androidModule = new AndroidModule(this);
}
return this.androidModule;
}
public MyCustomModule getMyCustomModule() {
if (this.myCustomModule == null) {
this.myCustomModule = new MyCustomModule();
}
return this.myCustomModule;
}
public EspressoRemoteResourcesModule getEspressoRemoteResourcesModule() {
if (this.espressoRemoteResourcesModule == null) {
this.espressoRemoteResourcesModule = new EspressoRemoteResourcesModule();
}
return this.espressoRemoteResourcesModule;
}
}
Mon test Espresso, sous app/src/androidTest/com/mypackage/espresso:
package com.mypackage.espresso;
// imports snipped for brevity
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MyActivityTest extends ActivityInstrumentationTestCase2<MyActivity>{
private MyActivity myActivity;
public MyActivityTest() {
super(MyActivity.class);
}
@Before
public void setUp() throws Exception {
super.setUp();
injectInstrumentation(InstrumentationRegistry.getInstrumentation());
myActivity = getActivity();
}
@After
public void tearDown() throws Exception {
super.tearDown();
}
@Test
public void testWhenTheActionBarButtonIsPressedThenThePlacesAreListed() {
//The next line is where the runtime exception occurs.
MyCustomEspressoApplication app = (MyCustomEspressoApplication)getInstrumentation().getTargetContext().getApplicationContext();
//I've also tried getActivity().getApplication() and
// getActivity.getApplicationContext() with the same results
//snip
}
}
Mon AndroidManifest.xml
(J'ai déjà vu de nombreuses réponses concernant l'exception ClassCastException dans les classes d'application personnalisées, et la plupart d'entre elles indiquent l'absence de la propriété "Android: name" sur le nœud de l'application. J'inscris cela ici pour montrer que ce n'est pas le cas. pour autant que je sache.)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:tools="http://schemas.Android.com/tools"
package="com.mypackage">
<!-- snip -->
<application
Android:name=".injection.MyCustomApplication"
Android:allowBackup="true"
Android:icon="@drawable/ic_launcher"
Android:label="@string/app_name"
Android:theme="@style/AppTheme" >
<!-- snip -->
</application>
<!-- snip -->
</manifest>
build.gradle
buildscript {
repositories {
mavenCentral()
jcenter()
}
}
apply plugin: 'com.Android.application'
apply plugin: 'idea'
Android {
testOptions {
unitTests.returnDefaultValues = true
}
lintOptions {
abortOnError false
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE'
}
compileSdkVersion 21
buildToolsVersion "21.1.2"
defaultConfig {
applicationId "com.mypackage"
minSdkVersion 15
targetSdkVersion 21
versionCode 1
versionName "1.0"
testInstrumentationRunner "Android.support.test.runner.AndroidJUnitRunner"
}
}
idea {
module {
testOutputDir = file('build/test-classes/debug')
}
}
dependencies {
compile project(':swipeablecardview')
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.Android.support:support-annotations:21.0.3'
compile 'com.Android.support:appcompat-v7:21.0.3'
compile 'com.squareup:javawriter:2.5.0'
compile ('com.squareup.dagger:dagger:1.2.2') {
exclude module: 'javawriter'
}
compile ('com.squareup.dagger:dagger-compiler:1.2.2') {
exclude module: 'javawriter'
}
compile 'com.melnykov:floatingactionbutton:1.1.0'
compile 'com.Android.support:cardview-v7:21.0.+'
compile 'com.Android.support:recyclerview-v7:21.0.+'
// compile 'se.walkercrou:google-places-api-Java:2.1.0'
compile 'org.Apache.httpcomponents:httpclient-Android:4.3.5.1'
compile 'commons-io:commons-io:1.3.2'
testCompile 'org.hamcrest:hamcrest-integration:1.3'
testCompile 'org.hamcrest:hamcrest-core:1.3'
testCompile 'org.hamcrest:hamcrest-library:1.3'
testCompile('junit:junit:4.12')
testCompile 'org.mockito:mockito-core:1.+'
testCompile('org.robolectric:robolectric:3.0-SNAPSHOT')
testCompile('org.robolectric:shadows-support-v4:3.0-SNAPSHOT')
androidTestCompile 'org.mockito:mockito-core:1.+'
androidTestCompile('com.Android.support.test.espresso:espresso-core:2.0') {
exclude group: 'javax.inject'
exclude module: 'javawriter'
}
androidTestCompile('com.Android.support.test:testing-support-lib:0.1')
}
Le stacktrace:
Jl.lass invokeNative (Méthode native) sur Java.lang.reflect.Method.invoke (Méthode.Java:511) sur org.junit.runners.model.FrameworkMethod $ 1.runReflectiveCall (FrameworkMethod.Java:45) sur org.junit.internal.runners .model.ReflectiveCallable.run (ReflectiveCallable.Java:15) à org.jun.runners.model.FrameworkMethod.invokeExplosively (FrameworkMethod.Java:42) à org.junit.internal.runners.statners.statants.statments.InvokeMetalal. : 20) à org.junit.internal.runners.statements.RunBefores.evaluate (RunBefores.Java:28) à org.junit.internal.runners.statements.RunAfters.evaluate (RunAfters.Java:30) à org.junit. runners.ParentRunner.runLeaf (ParentRunner.Java:263) à org.junit.runners.BlockJUnit4ClassRunner.runChild ( BlockJUnit4ClassRunner.Java:68) à org.junit.runners.BlockJUnit4ClassRunner.runChild (BlockJUnit4ClassRunner.Java:47) à .schedule (ParentRunner.Java:60) à org.junit.runners.ParentRunner.runChildren (ParentRunner.Java:229) à org.junit.runners.ParentRunner.access $ 000 (ParentRunner.Java:50) à org.junit.runners .ParentRunner $ 2.evaluate (ParentRunner.Java:222) à org.junit.runners.ParentRunner.run (ParentRunner.Java:300) à org.junit.runners.Suite.runChild (Suite.Java:128) à org.junit .runners.Suite.runChild (Suite.Java:24) à org.junit.runners.ParentRunner $ 3.run (ParentRunner.Java:231) à org.junit.runners.ParentRunner $ 3.run (ParentRunner.Java:231) à org.junit.runners.ParentRunner $ 1.schedule (ParentRunner.Java:60) à org.junit.runners.ParentRunner.runChildren (ParentRunner.Java:229) à org.junit.runners.ParentRunner.access $ 000 (ParentRunner.Java:50) à org.junit.runners.ParentRunner.access $ 000 (ParentRunner.Java:50) à org.junit.runners.ParentRunner.Agent (ParentRunner). 222) à org.junit.runners.ParentRunner.run (ParentRunner. Java: 300) à org.junit.runner.JUnitCore.run (JUnitCore.Java:157) à org.junit.runner.JunitCore.run (JUnitCore.Java:136) à Android.support.test.runner.AndroidJunitRunner.onStart (AndroidJUnitRunner.Java:270) sur Android.app.Instrumentation $ InstrumentationThread.run (Instrumentation.Java:1551)
J'ai lu les documents Espresso et Dagger et parcouru les problèmes sur Github sans succès. J'apprécierais toute aide que n'importe qui peut fournir. Merci d'avance.
Modifier # 1
J'ai suivi la suggestion de Daniel d'étendre le programme d'exécution de test et de vérifier le VerifyError, et j'ai obtenu la trace de pile suivante:
Java.lang.ExceptionInInitializerError
at org.mockito.internal.creation.cglib.ClassImposterizer.createProxyClass(ClassImposterizer.Java:95)
at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.Java:57)
at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.Java:49)
at org.mockito.internal.creation.cglib.CglibMockMaker.createMock(CglibMockMaker.Java:24)
at org.mockito.internal.util.MockUtil.createMock(MockUtil.Java:33)
at org.mockito.internal.MockitoCore.mock(MockitoCore.Java:59)
at org.mockito.Mockito.mock(Mockito.Java:1285)
at org.mockito.Mockito.mock(Mockito.Java:1163)
at com.mypackage.injection.EspressoRemoteResourcesModule.<init>(EspressoRemoteResourcesModule.Java:17)
at com.mypackage.injection.MyCustomEspressoApplication.getEspressoRemoteResourcesModule(MyCustomEspressoApplication.Java:52)
at com.mypackage.injection.MyCustomEspressoApplication.getModules(MyCustomEspressoApplication.Java:24)
at com.mypackage.injection.MyCustomApplication.onCreate(MyCustomApplication.Java:18)
at com.mypackage.injection.MyCustomEspressoApplication.onCreate(MyCustomEspressoApplication.Java:16)
at Android.app.Instrumentation.callApplicationOnCreate(Instrumentation.Java:999)
at Android.app.ActivityThread.handleBindApplication(ActivityThread.Java:4151)
at Android.app.ActivityThread.access$1300(ActivityThread.Java:130)
at Android.app.ActivityThread$H.handleMessage(ActivityThread.Java:1255)
at Android.os.Handler.dispatchMessage(Handler.Java:99)
at Android.os.Looper.loop(Looper.Java:137)
at Android.app.ActivityThread.main(ActivityThread.Java:4745)
at Java.lang.reflect.Method.invokeNative(Native Method)
at Java.lang.reflect.Method.invoke(Method.Java:511)
at com.Android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.Java:786)
at com.Android.internal.os.ZygoteInit.main(ZygoteInit.Java:553)
at dalvik.system.NativeStart.main(Native Method)
Caused by: Java.lang.VerifyError: org/mockito/cglib/core/ReflectUtils
at org.mockito.cglib.core.KeyFactory$Generator.generateClass(KeyFactory.Java:167)
at org.mockito.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.Java:25)
at org.mockito.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.Java:217)
at org.mockito.cglib.core.KeyFactory$Generator.create(KeyFactory.Java:145)
at org.mockito.cglib.core.KeyFactory.create(KeyFactory.Java:117)
at org.mockito.cglib.core.KeyFactory.create(KeyFactory.Java:109)
at org.mockito.cglib.core.KeyFactory.create(KeyFactory.Java:105)
at org.mockito.cglib.proxy.Enhancer.<clinit>(Enhancer.Java:70)
at org.mockito.internal.creation.cglib.ClassImposterizer.createProxyClass(ClassImposterizer.Java:95)
at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.Java:57)
at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.Java:49)
at org.mockito.internal.creation.cglib.CglibMockMaker.createMock(CglibMockMaker.Java:24)
at org.mockito.internal.util.MockUtil.createMock(MockUtil.Java:33)
at org.mockito.internal.MockitoCore.mock(MockitoCore.Java:59)
at org.mockito.Mockito.mock(Mockito.Java:1285)
at org.mockito.Mockito.mock(Mockito.Java:1163)
at com.mypackage.injection.EspressoRemoteResourcesModule.<init>(EspressoRemoteResourcesModule.Java:17)
at com.mypackage.injection.MyCustomEspressoApplication.getEspressoRemoteResourcesModule(MyCustomEspressoApplication.Java:52)
at com.mypackage.injection.MyCustomEspressoApplication.getModules(MyCustomEspressoApplication.Java:24)
at com.mypackage.injection.MyCustomApplication.onCreate(MyCustomApplication.Java:18)
at com.mypackage.injection.MyCustomEspressoApplication.onCreate(MyCustomEspressoApplication.Java:16)
at Android.app.Instrumentation.callApplicationOnCreate(Instrumentation.Java:999)
at Android.app.ActivityThread.handleBindApplication(ActivityThread.Java:4151)
at Android.app.ActivityThread.access$1300(ActivityThread.Java:130)
at Android.app.ActivityThread$H.handleMessage(ActivityThread.Java:1255)
at Android.os.Handler.dispatchMessage(Handler.Java:99)
at Android.os.Looper.loop(Looper.Java:137)
at Android.app.ActivityThread.main(ActivityThread.Java:4745)
at Java.lang.reflect.Method.invokeNative(Native Method)
at Java.lang.reflect.Method.invoke(Method.Java:511)
at com.Android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.Java:786)
at com.Android.internal.os.ZygoteInit.main(ZygoteInit.Java:553)
at dalvik.system.NativeStart.main(Native Method)
04-29 06:40:28.594 1016-1016/? W/ActivityManager﹕ Error in app com.mypackage running instrumentation ComponentInfo{com.mypackage.test/com.mypackage.EspressoTestRunner}:
04-29 06:40:28.594 1016-1016/? W/ActivityManager﹕ Java.lang.VerifyError
04-29 06:40:28.594 1016-1016/? W/ActivityManager﹕ Java.lang.VerifyError: org/mockito/cglib/core/ReflectUtils
Cela m'a dirigé vers Mockito. Il me manquait les bibliothèques nécessaires mockito et dexmaker.
J'ai mis à jour mes dépendances pour:
androidTestCompile 'org.mockito:mockito-core:1.10.19'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile ('com.google.dexmaker:dexmaker-mockito:1.2') {
exclude module: 'hamcrest-core'
exclude module: 'mockito-core'
}
androidTestCompile('com.Android.support.test.espresso:espresso-core:2.0') {
exclude group: 'javax.inject'
}
J'ai également remplacé MyCustomModule, qui devait inclure EspressoRemoteResourcesModule. Une fois que j'ai fait cela, les choses ont commencé à fonctionner.
Avec un programme d'instrumentation personnalisé, vous pouvez remplacer newApplication
et lui demander d'instancier autre chose que l'application par défaut à partir du manifeste.
public class MyRunner extends AndroidJUnitRunner {
@Override
public Application newApplication(ClassLoader cl, String className, Context context)
throws Exception {
return super.newApplication(cl, MyCustomEspressoApplication.class.getName(), context);
}
}
Assurez-vous de mettre à jour testInstrumentationRunner
avec le nom de votre coureur personnalisé.
Cela m'a pris une journée entière pour obtenir la réponse complète.
Étape 1: Ignorez AndroidJUnitRunner
public class TestRunner extends AndroidJUnitRunner
{
@Override
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return super.newApplication(cl, TestApplication.class.getName(), context);
}
}
Étape 2: remplacez AndroidJunitRunner existant dans build.gradle
defaultConfig {
...
// testInstrumentationRunner "Android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner 'com.xi_zz.androidtest.TestRunner'
}
Étape 3: Ajoutez com.Android.support.test: runner à build.gradle
androidTestCompile 'com.Android.support.test:runner:0.5'
androidTestCompile 'com.Android.support.test.espresso:espresso-core:2.2.2'
Étape 4: Seulement si vous avez cette erreur
Warning:Conflict with dependency 'com.Android.support:support-annotations'. Resolved versions for app (25.2.0) and test app (23.1.1) differ. See http://g.co/androidstudio/app-test-app-conflict for details.
Ensuite, ajoutez une ligne supplémentaire:
androidTestCompile 'com.Android.support:support-annotations:25.2.0'
androidTestCompile 'com.Android.support.test:runner:0.5'
androidTestCompile 'com.Android.support.test.espresso:espresso-core:2.2.2'
Enfin, vérifiez si cela fonctionne
@RunWith(AndroidJUnit4.class)
public class MockApplicationTest
{
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void testApplicationName() throws Exception
{
assertEquals("TestApplication", mActivityRule.getActivity().getApplication().getClass().getSimpleName());
}
}
Je n'ai pas largement essayé tous les cas, mais vous pouvez essayer une règle personnalisée pour spécifier votre classe d'application personnalisée par test, plutôt que pour tous les cas de test appliqués par le programme personnalisé. J'ai eu du succès avec les cas suivants dans des cas simples:
public class ApplicationTestRule<T extends Application> extends UiThreadTestRule {
Class<T> appClazz;
boolean wait = false;
T app;
public ApplicationTestRule(Class<T> applicationClazz) {
this(applicationClazz, false);
}
public ApplicationTestRule(Class<T> applicationClazz, boolean wait) {
this.appClazz = applicationClazz;
this.wait = wait;
}
@Override
public Statement apply(final Statement base, Description description) {
return new ApplicationStatement(super.apply(base, description));
}
private void terminateApp() {
if (app != null) {
app.onTerminate();
}
}
public void createApplication() throws IllegalAccessException, ClassNotFoundException, InstantiationException {
app = (T) InstrumentationRegistry.getInstrumentation().newApplication(this.getClass().getClassLoader(), appClazz.getName(), InstrumentationRegistry.getInstrumentation().getTargetContext());
InstrumentationRegistry.getInstrumentation().callApplicationOnCreate(app);
}
private class ApplicationStatement extends Statement {
private final Statement mBase;
public ApplicationStatement(Statement base) {
mBase = base;
}
@Override
public void evaluate() throws Throwable {
try {
if (!wait) {
createApplication();
}
mBase.evaluate();
} finally {
terminateApp();
app = null;
}
}
}
}
Ensuite, dans votre cas de test, créez la règle:
@Rule
public ApplicationTestRule<TestApplication> appRule = new ApplicationTestRule<>(TestApplication.class,true);
Notez que le deuxième paramètre est facultatif. Si la valeur est false ou désactivée, l'application personnalisée est créée chaque fois avant chaque scénario de test. Si défini sur true, vous devez appeler appRule.createApplication()
avant votre logique d'application.
Si vous testez un module de bibliothèque, vous pouvez créer une classe d'application personnalisée et l'enregistrer dans le manifeste du package de test:
racine/module-bibliothèque/src/androidTest/AndroidManifest.xml
<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android">
<application Android:name="path.to.TestApplication" />
</manifest>
Pas de règles, pas de coureurs.