Hilt Integration for Testing in Jetpack Compose: A Comprehensive Guide

When building Android applications with Jetpack Compose, testing is essential to ensure reliability and maintainability. Hilt, Google’s recommended dependency injection library for Android, simplifies dependency management and makes testing easier. This article will guide you through integrating Hilt for testing in your Jetpack Compose applications.

Why Use Hilt for Testing?

  • Simplified Dependency Management: Hilt automates dependency injection, reducing boilerplate code and making it easier to manage dependencies in your tests.
  • Testability: Hilt facilitates the replacement of real dependencies with mock or fake implementations, enabling effective unit and integration testing.
  • Component Scoping: Hilt’s component scopes help manage the lifecycle of dependencies, making it straightforward to simulate different scenarios in your tests.

Prerequisites

Before proceeding, make sure you have the following:

  • Android Studio installed.
  • Basic knowledge of Jetpack Compose.
  • Familiarity with Hilt dependency injection.
  • An existing Jetpack Compose project.

Step-by-Step Guide to Integrate Hilt for Testing

Step 1: Add Hilt Dependencies

First, add the necessary Hilt dependencies to your project’s build.gradle file:

// Top-level build.gradle
buildscript {
    ext {
        hilt_version = '2.48'
    }
}

dependencies {
    classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}

// app/build.gradle
plugins {
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

dependencies {
    implementation("com.google.dagger:hilt-android:$hilt_version")
    kapt("com.google.dagger:hilt-compiler:$hilt_version")
    
    // For instrumented tests
    androidTestImplementation("com.google.dagger:hilt-android-testing:$hilt_version")
    kaptAndroidTest("com.google.dagger:hilt-compiler:$hilt_version")
    
    // Jetpack Compose dependencies
    implementation("androidx.compose.ui:ui:$compose_version")
    debugImplementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
    debugImplementation("androidx.compose.ui:ui-tooling:$compose_version")
}

Remember to replace $hilt_version and $compose_version with your desired versions.

Step 2: Create a Hilt Test Application

Create a custom test application that uses Hilt. This application will replace the default application during testing.

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class HiltTestApplication : Application()

Next, declare this test application in your androidTest manifest:


    
    

Step 3: Define a Test Module

Create a Hilt module to provide test-specific dependencies. This allows you to replace real dependencies with mocks or fakes in your tests.

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object TestAppModule {

    @Singleton
    @Provides
    fun provideTestRepository(): MyRepository {
        return FakeMyRepository() // Using a fake repository
    }
}

Here, FakeMyRepository is a simple fake implementation of your repository, which you can use for testing:

class FakeMyRepository : MyRepository {
    override suspend fun getData(): Result {
        return Result.success("Test Data")
    }
}

Step 4: Create an Instrumented Test

Now, create an instrumented test that uses Hilt to inject dependencies. Annotate your test class with @HiltAndroidTest and use @Inject to obtain dependencies.

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MyComposeTest {

    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val composeTestRule = createComposeRule()

    @Inject
    lateinit var myRepository: MyRepository

    @Before
    fun setUp() {
        hiltRule.inject()
    }

    @Test
    fun testComposeContent() {
        // Set up your composable content with injected dependencies
        composeTestRule.setContent {
            MyComposable(myRepository = myRepository)
        }

        // Perform your UI tests using composeTestRule
        // Example: composeTestRule.onNodeWithText("Test Data").assertIsDisplayed()
    }
}

Explanation:

  • @HiltAndroidTest: Marks the test class as a Hilt test.
  • HiltAndroidRule: Manages the Hilt component lifecycle for the test.
  • @Inject: Injects the test dependencies.
  • createComposeRule(): Creates a Compose test rule to interact with your composable UI.
  • In setUp(), hiltRule.inject() performs the dependency injection before running the test.
  • MyComposable is a sample composable that requires MyRepository as a dependency.

@Composable
fun MyComposable(myRepository: MyRepository) {
    val data = produceState(initialValue = "Loading...") {
        value = myRepository.getData().getOrDefault("Error")
    }

    Text(text = "Data from Repository: ${data.value}")
}

Step 5: Run Your Tests

Finally, run your instrumented tests to verify that Hilt is correctly injecting test dependencies and that your UI behaves as expected.

Complete Example

Here’s a consolidated example for better clarity:

MyRepository.kt
interface MyRepository {
    suspend fun getData(): Result
}
MyRepositoryImpl.kt
import kotlinx.coroutines.delay
import javax.inject.Inject

class MyRepositoryImpl @Inject constructor() : MyRepository {
    override suspend fun getData(): Result {
        delay(100) // Simulate network delay
        return Result.success("Real Data")
    }
}
AppModule.kt
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Singleton
    @Provides
    fun provideMyRepository(): MyRepository {
        return MyRepositoryImpl()
    }
}
TestAppModule.kt
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object TestAppModule {

    @Singleton
    @Provides
    fun provideTestRepository(): MyRepository {
        return FakeMyRepository() // Using a fake repository
    }
}
FakeMyRepository.kt
class FakeMyRepository : MyRepository {
    override suspend fun getData(): Result {
        return Result.success("Test Data")
    }
}
MyComposable.kt

import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.produceState

@Composable
fun MyComposable(myRepository: MyRepository) {
    val data = produceState(initialValue = "Loading...") {
        value = myRepository.getData().getOrDefault("Error")
    }

    Text(text = "Data from Repository: ${data.value}")
}
MyComposeTest.kt

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.assertIsDisplayed
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MyComposeTest {

    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val composeTestRule = createComposeRule()

    @Inject
    lateinit var myRepository: MyRepository

    @Before
    fun setUp() {
        hiltRule.inject()
    }

    @Test
    fun testComposeContent() {
        // Set up your composable content with injected dependencies
        composeTestRule.setContent {
            MyComposable(myRepository = myRepository)
        }

        // Perform your UI tests using composeTestRule
        composeTestRule.onNodeWithText("Data from Repository: Test Data").assertIsDisplayed()
    }
}

Tips for Effective Testing

  • Use Fake Repositories: Always use fake or mock repositories in your tests to isolate the UI from external dependencies.
  • Test Different Scenarios: Create different test modules to simulate various scenarios, such as network errors or empty data states.
  • Write Focused Tests: Each test should focus on verifying a specific behavior of your UI.

Conclusion

Integrating Hilt into your Jetpack Compose testing strategy significantly improves the testability of your applications. By providing a clear and maintainable way to manage dependencies and replace them with test-specific implementations, Hilt simplifies writing effective unit and integration tests. Follow the steps outlined in this guide to ensure your Jetpack Compose applications are robust, reliable, and maintainable.