Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack compose navigation ui testing

I'm working with navigation testing in Jetpack Compose and I have Hilt implemented for DI in my project. But when I run a UI test to check navigation I encounter this Exception

Given component holder class androidx.activity.ComponentActivity does not implement interface dagger.hilt.internal.GeneratedComponent or interface dagger.hilt.internal.GeneratedComponentManager

This is what I have done this so far:

My Gradle file looks like this

buildscript {
    dependencies {
        classpath("com.google.dagger:hilt-android-gradle-plugin:2.48")
      }
}
plugins {
    id ("com.android.application") version "8.1.1" apply false
    id ("com.android.library") version "8.1.1" apply false
    id ("org.jetbrains.kotlin.android") version "1.8.21" apply false
    id ("com.google.dagger.hilt.android") version "2.48" apply false
    id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false
}
tasks.register("clean",Delete::class){
    delete(rootProject.buildDir)
}
plugins {
    id ("com.android.application")
    kotlin("android")
    kotlin("kapt")
    id ("dagger.hilt.android.plugin")
}

android {
    namespace = "com.example.boundarys"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.boundarys"
        minSdk = 26
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "com.example.boundarys.HiltTestRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_18
        targetCompatibility = JavaVersion.VERSION_18
    }
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_18.toString()
    }
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.4.7"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {
    coreLibraryDesugaring (libs.desugar.jdk.libs)
    implementation(project(":KtorModule"))
    implementation(project(":BxExtension"))

    implementation (libs.core.ktx)
    implementation (libs.lifecycle.runtime.ktx)
    implementation (libs.activity.compose)
    implementation (libs.ui)
    implementation (libs.ui.tooling.preview)
    implementation (libs.foundation)
    implementation(libs.material3)
    implementation("androidx.compose.material3:material3-window-size-class:1.2.0-alpha06")
    implementation(libs.androidx.material)

    // testing
    testImplementation (libs.junit)
    androidTestImplementation (libs.androidx.junit)
    androidTestImplementation (libs.androidx.espresso.core)
    androidTestImplementation (libs.androidx.ui.test.junit4)
    debugImplementation (libs.androidx.ui.tooling)
    debugImplementation (libs.androidx.ui.test.manifest)
    testImplementation (libs.truth)
    androidTestImplementation (libs.truth)
    androidTestImplementation (libs.androidx.core.testing)
    testImplementation(libs.kotlinx.coroutines.test)
    androidTestImplementation(libs.kotlinx.coroutines.test)
    testImplementation(libs.androidx.core.testing)
    androidTestImplementation(libs.mockito.core)
    androidTestImplementation(libs.mockito.kotlin)
    androidTestImplementation(libs.androidx.navigation.testing)

    implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))

    // Compose dependencies
    implementation (libs.androidx.lifecycle.viewmodel.compose)
    implementation (libs.androidx.navigation.compose)
    implementation ("androidx.compose.material:material-icons-extended")



    // Dependency Injection
    implementation(libs.hilt.android)
    kapt(libs.hilt.android.compiler)
    implementation(libs.androidx.hilt.work)
    kapt(libs.androidx.hilt.compiler)
    implementation(libs.androidx.work.runtime.ktx)
    implementation(libs.hilt.navigation.compose)

    // For Robolectric tests.
    testImplementation (libs.hilt.android.testing)
    // ...with Kotlin.
    kaptTest (libs.hilt.android.compiler)
    // ...with Java.
    testAnnotationProcessor (libs.hilt.android.compiler)


    // For instrumented tests.
    androidTestImplementation (libs.hilt.android.testing)
    // ...with Kotlin.
    kaptAndroidTest (libs.hilt.android.compiler)
    // ...with Java.
    androidTestAnnotationProcessor (libs.hilt.android.compiler)

    // Lifecycle
    implementation(libs.androidx.lifecycle.viewmodel.compose)
    implementation(libs.androidx.lifecycle.viewmodel.ktx)
    implementation(libs.androidx.lifecycle.runtime.compose)

    // Room
    implementation(libs.androidx.room.runtime)
    implementation(libs.androidx.room.ktx)
    implementation(libs.androidx.runtime.livedata)
    kapt(libs.androidx.room.compiler)
    annotationProcessor( libs.androidx.room.compiler)

    // constraint layout
    implementation (libs.androidx.constraintlayout.compose)

    // Glide
    implementation (libs.landscapist.glide)

    // GSON
    api(libs.gson)
}

I have this HiltTestRunner File and added to gradle you can see

testInstrumentationRunner = "com.example.boundarys.HiltTestRunner"

File look like this

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

    }
}

My Activity is like this and define the application file in Manifest as well

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        init()
        setContent {
            BoundarysTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    BoundarysNavGraph()
                }
            }
        }
    }
}
@HiltAndroidApp
class BoundarysApp : Application(){
}

I have use nested graph for implementation

@Composable
fun BoundarysNavGraph(navController: NavHostController = rememberNavController()){
    NavHost(
        navController = navController,
        startDestination =BoundarysNavRoutes.Splash.route){
        navigation(
            startDestination =SplashScreenRoutes.SPLASH.route ,
            route=BoundarysNavRoutes.Splash.route,
        ){
            splashNavGraph(navController)
        }
    }
}


fun NavGraphBuilder.splashNavGraph(navController: NavHostController){
    horizontallyAnimatedComposableEnterOnly(SplashScreenRoutes.SPLASH.route){
        val viewModel = hiltViewModel<SplashViewModel>()
        SplashScreen(navController)
    }
    horizontallyAnimatedComposable(SplashScreenRoutes.LANDING.route){
        val viewModel = hiltViewModel<LandingPageViewModel>()
        LandingPageScreen(navController,viewModel.eventFlow){
            viewModel.onEventUpdate(it)
        }
    }
    horizontallyAnimatedComposable(SplashScreenRoutes.EXPLAINER.route){
        val viewModel = hiltViewModel<ExplainerScreenViewModel>()
        ExplainerScreen(navController,viewModel.eventFlow){
            viewModel.onEventUpdate(it)
        }
    }
}

And My test file looks like this

@HiltAndroidTest
class SplashNavTest {

    @get:Rule
    val composeTestRule =  createComposeRule()
    @get:Rule
    var hiltRule = HiltAndroidRule(this)
    private lateinit var context: Context

    private lateinit var navController: TestNavHostController

    @Before
    fun init(){
        hiltRule.inject()
        context = ApplicationProvider.getApplicationContext<Context>()
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            BoundarysNavGraph(navController = navController)
        }
    }

    @Test
    fun Verify_SplashIsVisible(){
        val currentDestination = navController.currentBackStackEntry?.destination?.route

        Truth.assertThat(currentDestination).isEqualTo(SplashScreenRoutes.SPLASH.route)
    }

}
like image 355
Mohsin Ali Avatar asked Jun 07 '26 11:06

Mohsin Ali


1 Answers

The first problem in your code lies in the fact that you have not specified the order for the composeTestRule and hiltRule. Besides that, in order to be able to access the corresponding activity, the composeTestRule should be created using createAndroidComposeRule. So your code should be changed as below:

@get:Rule(order = 0) //👈
var hiltRule = HiltAndroidRule(this)

@get:Rule(order = 1) //👈
val composeTestRule = createAndroidComposeRule<MainActivity>() //👈

Now, when setting the content, remember to specify the activity:

composeTestRule.activity.setContent { ... }
//                👆
like image 70
Alex Mamo Avatar answered Jun 10 '26 04:06

Alex Mamo



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!