android-test icon indicating copy to clipboard operation
android-test copied to clipboard

espresso-contrib makes ActivityScenarioRule trigger ClassNotFoundException

Open mgiuffrida opened this issue 3 years ago • 4 comments

Description

If espresso-contrib is added to build.gradle, then adding an ActivityScenarioRule to a simple androidTest causes a fatal ClassNotFoundException.

Steps to Reproduce

  1. Start with a simple Android Studio project. I used the "Blank Activity" template with kotlin and SDK 27.
  2. Add to build.gradle: androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
  3. Add a new member variable to an androidTest, e.g. ExampleInstrumentedTest:
@get:Rule
val rule = ActivityScenarioRule(MainActivity::class.java)
  1. Launch an AVD (I used Android 11.0 (Google APIs) on Pixel 5)
  2. Run the test

Expected Results

The test should pass.

Actual Results

The test hangs and eventually terminates. Immediately after the test starts, logcat reports a crash:

2021-09-25 02:19:35.656 10153-10153/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.foo.ruletest.test, PID: 10153
    java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/test/internal/util/Checks;
        at androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity.onResume(InstrumentationActivityInvoker.java:169)
        at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1456)
        at android.app.Activity.performResume(Activity.java:8135)
        at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4434)
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4476)
        at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.ClassNotFoundException: Didn't find class "androidx.test.internal.util.Checks" on path: DexPathList[[zip file "/data/app/~~dPOmzOwzgsQWBjEGt2CgBA==/com.foo.ruletest.test-3LocvMQ2WLgtGbjdq_FpWA==/base.apk"],nativeLibraryDirectories=[/data/app/~~dPOmzOwzgsQWBjEGt2CgBA==/com.foo.ruletest.test-3LocvMQ2WLgtGbjdq_FpWA==/lib/x86_64, /system/lib64, /system_ext/lib64]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:207)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity.onResume(InstrumentationActivityInvoker.java:169) 
        at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1456) 
        at android.app.Activity.performResume(Activity.java:8135) 
        at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4434) 
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4476) 
        at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52) 
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:223) 
        at android.app.ActivityThread.main(ActivityThread.java:7656) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

AndroidX Test and Android OS Versions

    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    testImplementation 'junit:junit:4.13.2'
    implementation 'androidx.test.espresso:espresso-contrib:3.4.0'

Android 11.0 (Google APIs) on virtual Pixel 5

Other

This doesn't seem to repro if espresso-contrib is used as androidTestImplementation instead of as implementation. But a number of examples I've found use it as implementation.

I ran into this while trying to test SettingsFragment. I found a few similar (unsolved) reports:

mgiuffrida avatar Sep 25 '21 07:09 mgiuffrida

Using any androidx.test library as an implementation dependency instead of an androidTestImplementation sounds like a misconfiguration to me.

That being said this still seems like a bug. I can repro on github/android/testing-samples/ui/espresso/RecyclerViewSample

With this configuration androidx.test.internal.util.Checks ends up in the app-under-test apk. I suspect this might be related to #832 - where ActivityScenario states the bootstrap activities in a test process, which might not have the app-under-test apk in classpath.

One workaround is to also put androidx.test.core in implementation implementation 'androidx.test:core:' + rootProject.coreVersion;

@yuuki3655 WDYT?

I'd be in favor of build systems systems systems dropping the dual apk configuration when running instrumentation tests, and just produce a single self-instrumented apk

brettchabot avatar Sep 25 '21 20:09 brettchabot

Agreed that it's probably a misconfiguration. I'm brand new to Android development, just playing around with it for fun really.

But it took me hours to identify the cause of this crash. There's really nothing obviously pointing to the implementation dependency as the root cause; I spent hours paring down my project to the bare minimum that would repro, despite how simple the repro instructions look now. (And I might be new to Android, but I'm also a software engineer at Google, so I imagine plenty of people run into this mistake.)

Just to say, thanks for looking into this!

mgiuffrida avatar Sep 25 '21 20:09 mgiuffrida

Yes, I agreed that the error message is confusing and doesn't help much. A single APK, self-instrumenting test would simplify the setup. You can enable self-instrumenting tests by setting the following flag. This is added recently and still in the experimental feature though.

android {
    experimentalProperties["android.experimental.self-instrumenting"] = true
}

Adding test specific dependencies to a produce APK is not really preferable as it increases the APK size. We might want to display a warning when you try to add a dependency to a product code that is known for test library.

yuuki3655 avatar Sep 28 '21 18:09 yuuki3655

I can consistently duplicate this case with and without androidTestImplement "androidx.test.espresso:espresso-contrib:3.4.0", which makes me think it isn't at all related to the issue:

  • Pixel 4 API 31 w/Google APIs
    @get:Rule(order = 1)
    val composeTestRule = createComposeRule() //This function is being pulled from androidx.compose.ui.test.junit4

    @get:Rule(order = 2)
    var activityRule: ActivityScenarioRule<MainActivity>
           = ActivityScenarioRule(MainActivity::class.java)
    // Instrumentation Testing
    androidTestImplementation("androidx.test:core-ktx:1.4.0")
    androidTestImplementation('androidx.test:rules:1.4.0')
    androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha06'
    androidTestImplementation 'androidx.test:runner:1.5.0-alpha03'

    def espresso_version = "3.4.0"
    def compose_version = "1.2.0-beta02"
    def hilt_version = "2.38.1"
    androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espresso_version"
    androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espresso_version"
    androidTestImplementation("com.google.crypto.tink:tink-android:HEAD-SNAPSHOT")
    androidTestImplementation("androidx.compose.ui:ui-test:$compose_version") {
        exclude group: "androidx.test.espresso"
    }
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
    androidTestImplementation("com.google.dagger:hilt-android-testing:$hilt_version")

The (unfortunate) work-around @brettchabot mentioned about making androidx.test:core-ktx implementation does work, but I think we can all agree that is less than ideal. I'm duplicating this on an internal project at the moment, but I'll try to get a simple demo app to make duplication easier.

Edit

OK, so clarifying details about the work-around: That only seems to work for me in Android Studio, when I'm not running the tests via Gradle. It does not work for me when I run tests via ./gradlew connectedCheck.

  • Android Studio Chipmunk | 2021.2.1 Patch 1
  • Gradle version 7.3.3
  • classpath com.android.tools.build:gradle:7.2.1

jameskbride avatar May 31 '22 12:05 jameskbride