Могу ли я расширить пользовательское приложение в Espresso?

Я пытаюсь настроить кинжал в своих тестах на эспрессо, чтобы издеваться над внешними ресурсами (в этом случае RESTful-сервисы). Образец, который я последовал в Robolectric для моего модульного тестирования, заключался в расширении моего класса Application Application и переопределении модулей Dagger с помощью тестовых модулей, которые возвратят насмешки. Я пытаюсь сделать то же самое здесь, но я получаю ClassCastException в своих тестах Espresso, когда пытаюсь применить приложение к своему пользовательскому приложению.

Вот моя настройка:

Продукция

В приложении /src/main/java/com/mypackage/injection у меня есть:

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

Что я использую следующим образом:

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

}

Проверенная активность

package com.mypackage.views.mydomain;
// imports snipped for bevity

public class MyActivity extends MyBaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //snip
    }
}

Настройка эспрессо

В приложении /src/androidTest/java/com/mypackage/injection У меня есть:

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

Мой тест на эспрессо в приложении /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
     }
}

Мой AndroidManifest.xml

(Я видел много ответов относительно ClassCastException в пользовательских классах приложений раньше, и большинство из них указывают на отсутствие свойства "android: name" в приложении node. Я вставляю это здесь, чтобы показать, что это не так, насколько я могу судить.)

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

Стек:

java.lang.ClassCastException: com.mypackage.injection.MyCustomApplication нельзя отбрасывать com.mypackage.injection.MyCustomEspressoApplication at com.mypackage.espresso.MyActivityTest.testWhenTheActionBarButtonIsPressedThenThePlacesAreListed(MyActivityTest.java:107) в java.lang.reflect.Method.invokeNative(собственный метод) в java.lang.reflect.Method.invoke(Method.java:511) в org.junit.runners.model.FrameworkMethod $1.runReflectiveCall(FrameworkMethod.java:45) в org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) в org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42) в org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java: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) на 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 $2.оценить (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 $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 $2.оценить (ParentRunner.java: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) в android.app.Instrumentation $InstrumentationThread.run(Instrumentation.java:1551)

Я прочитал документы эспрессо и кинжала и искал проблемы в Github безрезультатно. Буду признателен за любую помощь, которую любой может предоставить. Заранее спасибо.

Изменить # 1

Я последовал за предложением Дэниэла продлить тестовый бегун и проверить VerifyError и получил следующую трассировку стека:

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

Это указывало на Мокито. Мне не хватало необходимых библиотек mockito и dexmaker.

Я обновил свои зависимости до:

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

Я также переопределял MyCustomModule, который должен был включать EspressoRemoteResourcesModule. Как только я это сделал, это начало работать.

Ответ 1

С помощью специального инструментария вы можете переопределить newApplication и создать экземпляр чего-либо, кроме приложения по умолчанию из манифеста.

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

Обязательно обновите testInstrumentationRunner своим пользовательским именем.

Ответ 2

Принял меня целый день, чтобы получить полный ответ.

Шаг 1: переопределить 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);
    }
}

Шаг 2: замените существующий AndroidJunitRunner в файле build.gradle

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

Шаг 3: Добавьте 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'

Шаг 4: Только если вы получили эту ошибку

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.

Затем добавьте еще одну строку:

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'

Наконец, проверьте, работает ли он

@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

Я не очень разбирался во всех случаях, но вы можете попробовать собственное правило, чтобы указать свой собственный класс приложения для каждого тестового примера, а не для всех тестовых случаев, применяемых пользовательским бегуном. В простых случаях я добился успеха в следующем:

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

Затем в тестовом примере создайте правило:

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

Обратите внимание, что второй параметр является необязательным. Если значение false или прекращено, пользовательское приложение создается каждый раз перед каждым тестовым случаем. Если установлено значение true, вам нужно вызвать appRule.createApplication() перед вашей логикой приложения.