Can I extend a custom Application in Espresso?

时光毁灭记忆、已成空白 提交于 2019-11-27 02:32:22

问题


I'm attempting to set up Dagger in my Espresso instrumentation tests in order to mock out calls to external resources (RESTful services in this case). The pattern I followed in Robolectric for my unit testing was to extend my production Application class and override the Dagger modules with test modules which will return mocks. I'm attempting to do the same thing here, but I'm getting a ClassCastException in my Espresso tests when I try to cast the application to my custom application.

Here is my set up thus far:

Production

Under app/src/main/java/com/mypackage/injection I have:

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);
    }
}

Which I use in the following manner:

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);
    }

}

Activity under 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
    }
}

Espresso Setup

Under app/src/androidTest/java/com/mypackage/injection I have:

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;
    }
}

My Espresso test, under 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
     }
}

My AndroidManifest.xml

(I've seen lots of answers regarding the ClassCastException in custom Application classes before, and most of them point to a missing "android:name" property on the Application node. I'm pasting this here to show that this is not the case as far as I can tell.)

<?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')
}

The stacktrace:

java.lang.ClassCastException: com.mypackage.injection.MyCustomApplication cannot be cast to com.mypackage.injection.MyCustomEspressoApplication at com.mypackage.espresso.MyActivityTest.testWhenTheActionBarButtonIsPressedThenThePlacesAreListed(MyActivityTest.java:107) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:511) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28) at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:30) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222) at org.junit.runners.ParentRunner.run(ParentRunner.java:300) at org.junit.runners.Suite.runChild(Suite.java:128) at org.junit.runners.Suite.runChild(Suite.java:24) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222) at org.junit.runners.ParentRunner.run(ParentRunner.java:300) at org.junit.runner.JUnitCore.run(JUnitCore.java:157) at org.junit.runner.JUnitCore.run(JUnitCore.java:136) at android.support.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:270) at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1551)

I've read through the Espresso and Dagger docs and searched through issues on Github to no avail. I'd appreciate any help anyone can provide. Thanks in advance.

Edit #1

I followed Daniel's suggestion to extend the test runner and checkout the VerifyError, and got the following stack trace:

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

This pointed me at Mockito. I was missing the necessary mockito and dexmaker libraries.

I updated my dependencies to:

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'
}

I also overrode MyCustomModule, which needed to include EspressoRemoteResourcesModule. Once I did this things started working.


回答1:


With a custom instrumentation runner, you can override newApplication and have it instantiate something other than the default application from the manifest.

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);
  }
}

Be sure to update testInstrumentationRunner with your custom runner's name.




回答2:


Took me an entire day to get the complete answer.

Step 1: Override 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);
    }
}

Step 2: replace the existing AndroidJunitRunner in build.gradle

defaultConfig {
    ...
    // testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    testInstrumentationRunner 'com.xi_zz.androidtest.TestRunner'
}

Step 3: Add com.android.support.test:runner to build.gradle

androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'

Step 4: Only if you got this error

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.

Then, add one more line:

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'

Finally, test if it works

@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());
    }
}



回答3:


If you're testing a library module you can make a custom application class and register it in the test package manifest:

root/library-module/src/androidTest/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application android:name="path.to.TestApplication" />
</manifest>

No rules, no runners.




回答4:


I haven't tried this extensively all cases, but you can try a custom Rule to specify your Custom Application class per test case instead of for all test cases applied by the custom runner. I've had success with the following in simple cases:

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;
            }
        }
    }
}

Then in your test case, create the rule:

@Rule
public ApplicationTestRule<TestApplication> appRule = new ApplicationTestRule<>(TestApplication.class,true);

Note the second parameter is optional. If false or left off, the custom application is created each time before each test case. If set to true, you need to call appRule.createApplication() before your Application logic.



来源:https://stackoverflow.com/questions/29750164/can-i-extend-a-custom-application-in-espresso

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!