Сбой тестов Android при использовании Hilt, вызванный ClassCastException

#android #android-studio #testing #dagger-hilt

Вопрос:

Я уже довольно давно пишу код kotlin для приложений Android, но решил также начать писать код тестирования для своих приложений. Однако у меня возникли некоторые проблемы с использованием рукояти. То, что я пытался, это :

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

 

мой тестовый класс :

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

 

my app level gradle file :

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

 

(Above I added only the dependencies used for testing )
project 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
}
 

Я следовал официальной документации от рукояти кинжала на этой странице : 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)

 

и я не знаю, как это исправить, потому что я действительно новичок в тестировании и сталкиваюсь с этим впервые !
строка 14 в HIltTestRunner-это :
вернуть супер.новое приложение(cl,HiltTestApp::class.java.name, контекст)

Комментарии:

1. нашел решение в github Google

Ответ №1:

У меня была та же проблема, во время тестирования я получил ClassCastException: HiltTestApplication cannot be cast to AbcApp (мой класс приложений).

Решение заключается в @CustomTestApplication аннотации, см. Документы Dagger или Документы разработчиков Android и использование созданного класса

 <interfacename>_Application.
 

Далее имейте в виду, что простое использование IgmeApplication (или AbcApp в моем случае) невозможно при использовании @HiltAndroidApp аннотации, см. раздел «Открытая проблема«. Тогда вы должны сделать open class AbstractApplication: Application() то же, что сделал спрашивающий. Который затем подразделяется вашим приложением (AbcApp в моем случае или IgmeApplication в случае с вопросами) и подразделяется созданным классом из аннотации @CustomTestApplication. Нравится:

 @CustomTestApplication(AbstractApplication::class)
interface CustomTestApplicationForHilt
 

Обратите внимание , что interface используется вместо open class , но он также работает с открытым классом, как это сделал спрашивающий.

Созданный класс затем CustomTestApplicationForHilt_Application должен использоваться при вызове newApplication в вашем классе Testrunner, например:

  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), как хотите, но окончание _Application будет добавлено к имени класса, когда аннотация будет обработана во время создания приложения. Также обратите внимание, что класс не будет доступен до сборки.