Android 使用 Hilt 时测试失败,由 ClassCastException 引起

Android Tests failing when using Hilt caused by ClassCastException

我已经为 android 应用程序编写 kotlin 代码已有一段时间了,但我决定也开始为我的应用程序编写测试代码。我在使用 Hilt 时遇到了一些问题。我试过的是:


    import android.app.Application
    
    open class AbstractApplication: Application()


    @HiltAndroidApp
    class IgmeApplication : IgmeAbstractApplication() {  
    
      @Inject
      lateinit var authenticationManager: AuthenticationManager

             ....
}

然后在 Android 测试目录中:


    import dagger.hilt.android.testing.CustomTestApplication
    
    @CustomTestApplication(AbstractApplication::class)
    open class HiltTestApplication


    import android.app.Application
    import android.content.Context
    import androidx.test.runner.AndroidJUnitRunner
    
    class HiltTestRunner : AndroidJUnitRunner() {
    
        override fun newApplication(
            cl: ClassLoader?,
            className: String?,
            context: Context?
        ): Application {
            return super.newApplication(cl,HiltTestApp::class.java.name, context)
        }
    }

我的测试class:


    @HiltAndroidTest
    class AuthenticationTest{
    
        @get:Rule
        var hiltRule = HiltAndroidRule(this)
    
        @Test
        fun useAppContext() {
            // Context of the app under test.
            val appContext = InstrumentationRegistry.getInstrumentation().targetContext
            Assert.assertEquals("com.crowdpolicy.onext.igme", appContext.packageName)
        }
    
        @Before
        fun setUp() {
            // Populate @Inject fields in test class
            hiltRule.inject()
        }
    
        @After
        fun tearDown() {
        }
    }

我的应用级别gradle 文件:


    plugins {
        id 'com.android.application'
        id 'kotlin-android'
        id 'kotlin-kapt'
        id 'com.google.gms.google-services'
        id 'com.google.firebase.crashlytics'
        id 'com.google.firebase.firebase-perf' // Firebase Performance monitoring
        id 'androidx.navigation.safeargs.kotlin'
        id 'dagger.hilt.android.plugin'
        id 'kotlin-parcelize'
        id 'com.google.protobuf'
    }
    
    Properties localProperties = new Properties()
    localProperties.load(new FileInputStream(rootProject.file('local.properties')))
    
    Properties keyStoreProperties = new Properties()
    keyStoreProperties.load(new FileInputStream(rootProject.file('keystore.properties')))
    
    android {
        buildToolsVersion "30.0.3"
        ndkVersion localProperties['ndk.version']
    
        signingConfigs {
            release {
                storeFile file(keyStoreProperties['key.release.path'])
                keyAlias 'igme-key'
                storePassword keyStoreProperties['key.release.keystorePassword']
                keyPassword keyStoreProperties['key.release.keyPassword']
            }
        }
    
        compileSdkVersion 30
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    
     kotlinOptions {
            jvmTarget = JavaVersion.VERSION_1_8.toString()
            freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn'
        }
    
        defaultConfig {
            applicationId "com.crowdpolicy.onext.igme"
            minSdkVersion 21
            targetSdkVersion 30
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "com.crowdpolicy.onext.igme.HiltTestRunner"
        }
    buildTypes {
            release {
                minifyEnabled true
                shrinkResources true
    
                debuggable false
    
                //signingConfig signingConfigs.release
    
                firebaseCrashlytics {
                    // Enable processing and uploading o FirebaseCrashlytics.getInstance()f native symbols to Crashlytics
                    // servers. By default, this is disabled to improve build speeds.
                    // This flag must be enabled to see properly-symbolicated native
                    // stack traces in the Crashlytics dashboard.
                    nativeSymbolUploadEnabled true
                    unstrippedNativeLibsDir "$buildDir/ndklibs/libs"
                }
    
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    
                ndk.debugSymbolLevel = "FULL" // Generate native debug symbols
            }
        }
    
        packagingOptions {
            exclude 'META-INF/DEPENDENCIES'
            exclude 'META-INF/rxjava.properties'
        }
    
        kotlinOptions {
            jvmTarget = '1.8'
        }
    
        testOptions {
            unitTests {
                includeAndroidResources = true
            }
        }
    
        android.buildFeatures.viewBinding = true
    
      dependencies {
     // Testing-only dependencies
            testImplementation 'junit:junit:4.13.2'
    
            // Core library
            androidTestImplementation 'androidx.test:core:1.4.0'
    
            // AndroidJUnitRunner and JUnit Rules
            androidTestImplementation 'androidx.test:runner:1.4.0'
            androidTestImplementation 'androidx.test:rules:1.4.0'
    
            // Assertions
            androidTestImplementation 'androidx.test.ext:junit:1.1.3'
            androidTestImplementation 'androidx.test.ext:truth:1.4.0'
            
            // Espresso dependencies
            androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
            androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-web:$espresso_version"
            androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espresso_version"
    
            // The following Espresso dependency can be either "implementation"
            // or "androidTestImplementation", depending on whether you want the
            // dependency to appear on your APK's compile classpath or the test APK
            // classpath.
            androidTestImplementation "androidx.test.espresso:espresso-idling-resource:$espresso_version"
    
      //Dagger
            implementation "com.google.dagger:dagger:$dagger_version"
            kapt "com.google.dagger:dagger-compiler:$dagger_version"
    
            // region Hilt
            implementation "com.google.dagger:hilt-android:$hilt_version"
            implementation "androidx.hilt:hilt-navigation-fragment:$hilt_fragment_version"
            kapt "com.google.dagger:hilt-android-compiler:$hilt_version" // or :  kapt 'com.google.dagger:hilt-compiler:2.37'
    
            // Testing Navigation
            androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
    
            // region Hilt testing - for instrumentation tests
            androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
            // Make Hilt generate code in the androidTest folder
            kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
            // endregion
    
            // For local unit tests
            testImplementation 'com.google.dagger:hilt-android-testing:2.37'
            kaptTest 'com.google.dagger:hilt-compiler:2.37'
    
            androidTestImplementation 'com.google.dagger:hilt-android-testing:2.37'
      }
    }
    
    
    kapt {
        correctErrorTypes true
        javacOptions {
            // These options are normally set automatically via the Hilt Gradle plugin, but we
            // set them manually to workaround a bug in the Kotlin 1.5.20: https://github.com/google/dagger/issues/2684
            option("-Adagger.fastInit=ENABLED")
            option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
        }
    }
    
    // https://github.com/google/protobuf-gradle-plugin
    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:$protobuf_version"
            //    path = localProperties["protoc.dir"]
        }
    
        // Generates the java Protobuf-lite code for the Protobufs in this project. See
        // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
        // for more information.
        generateProtoTasks {
            all().each { task ->
                task.builtins {
                    java {
                        option 'lite'
                    }
                }
            }
        }
    }
    
    dependencies {
        implementation 'androidx.legacy:legacy-support-v4:1.0.0'
        implementation 'androidx.appcompat:appcompat:1.3.1'
        implementation 'com.google.android.material:material:1.4.0'
        implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
    }

(上面我只添加了测试用的依赖) 项目 gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = "1.5.20"
    ext.google_version = '4.3.4'
    ext.timberVersion = '4.7.1'
    ext.event_bus = '3.2.0'
    ext.gson_version = '2.8.6'
    ext.retrofit2_version = '2.9.0'
    ext.datastore_version = '1.0.0-rc02'
    ext.rxkotlin_version = '3.0.1'
    ext.rxandroid_version = '3.0.0'
    ext.lifecycle_version = '2.3.1'
    ext.dagger_version = '2.37'
    ext.hilt_version = '2.37'
    ext.hilt_fragment_version = '1.0.0'
    ext.nav_version = '2.3.5'
    ext.fragment_version = '1.3.6'
    ext.androidXTestCoreVersion = '1.4.0'
    ext.espresso_version = '3.4.0'
    ext.lottie_version = '3.6.1'
    ext.facebook_version = '9.0.0'
    ext.protobuf_version = '3.15.8'
    ext.protobuf_gradle_plugin_version = '0.8.16'

    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.2.2"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "com.google.gms:google-services:$google_version"
        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
        classpath 'com.google.firebase:perf-plugin:1.4.0'
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
        classpath "com.google.protobuf:protobuf-gradle-plugin:$protobuf_gradle_plugin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter() // Warning: this repository is going to shut down soon
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

我在本页中遵循了 dagger-hilt 的官方文档:https://dagger.dev/hilt/testing.html,但我仍然收到此错误:


         Caused by: java.lang.ClassCastException: com.crowdpolicy.onext.igme.HiltTestApplication cannot be cast to android.app.Application
     at android.app.Instrumentation.newApplication(Instrumentation.java:997)
            at android.app.Instrumentation.newApplication(Instrumentation.java:982)
            at com.crowdpolicy.onext.igme.HiltTestRunner.newApplication(HiltTestRunner.kt:14)
            at android.app.LoadedApk.makeApplication(LoadedApk.java:617)

而且我不知道如何解决它,因为我真的是测试新手,这是第一次面对它! HIltTestRunner 中的第 14 行是: return super.newApplication(cl,HiltTestApp::class.java.name, 上下文)

我遇到了同样的问题,在测试时我得到了 ClassCastException: HiltTestApplication cannot be cast to AbcApp(我的申请 class)。

解决方法是@CustomTestApplication注解,见Dagger Docs or Android Dev Docs和使用生成的class

<interfacename>_Application.

进一步注意,当它使用 @HiltAndroidApp 注释时,仅使用 IgmeApplication(在我的例子中是 AbcApp)是不可能的,参见 open Issue。然后你必须像提问者那样做一个open class AbstractApplication: Application()。然后由您的应用程序(在我的情况下为 AbcApp 或在提问者的情况下为 IgmeApplication)进行 subclass 编辑,并由注释 @CustomTestApplication 创建的 class 进行 subclass 编辑。喜欢:

@CustomTestApplication(AbstractApplication::class)
interface CustomTestApplicationForHilt

请注意,使用 interface 而不是 open class,但它也可以像提问者那样使用 open class。

然后创建的 class 是 CustomTestApplicationForHilt_Application,在您的 Testrunner class 中调用 newApplication 时必须使用它,例如:

 class HiltTestRunner : AndroidJUnitRunner() {
    override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
        return super.newApplication(cl, CustomTestApplicationForHilt_Application::class.java.name, context)
    }
}

请注意,您可以根据需要为@CustomTestApplication 注解(此处为CustomTestApplicationForHilt)选择接口名称,但注解时会在class 名称中添加结尾_Application在构建应用程序时进行处理。另请注意,class 在构建之前将不可用。