web-dev-qa-db-fra.com

Puis-je étendre une application personnalisée dans Espresso?

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.

18
jameskbride

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é.

26
Daniel Lubarov

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());
    }
}
16
Xi Wei

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.

3
JCricket

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.

1
Eugen Pechanec