Linuxias
Developer's Delight
Linuxias
  • Category
    • AI
      • Deep Learning
      • Machine Learning
      • Data Science
      • Framework
      • MLOps
      • Paper-Review
      • Tips
    • Android
      • Kotlin
      • Component
      • Compose
      • Compose UI
      • Material
      • Testing
    • Software Architecture
      • Architecture Pattern
      • Design Pattern
      • Requirement Engineering
    • Linux
      • Compile & Link
      • Command & Tool
      • Container
      • Debugging & Testing
      • Profiling
      • Kernel Analysis
      • Server
      • Shell Script
      • System Programming
    • Language
      • Carbon
      • C,C++
      • C#
      • Java
      • Python
    • ETC
      • Data Struct | Algorithm
      • git
      • Security
    • Book
    • 경제공부
      • 세금
      • 부동산
hELLO · Designed By 정상우.
Linuxias

Developer's Delight

[Android/Testing] #8. 테스트에 Dagger-Hilt 적용해보기
Android/Testing

[Android/Testing] #8. 테스트에 Dagger-Hilt 적용해보기

2022. 11. 8. 15:50
반응형

Dagger-Hilt(이하 Hilt)와 같은 DI(Dependency Injection, 의존성 주입) 프레임워크를 사용하여 얻을 수 있는 장점 중 하나는 코드를 더 쉽게 테스트할 수 있다는 점이다. 테스트하려는 각 컴포넌트, 모듈들은 독립적으로 테스트 되어야 한다. 하지만 각 컴포넌트 들은 상호간의 관계, 즉 의존성을 가지고 각자의 역할을 수행하거나 필요한 역할을 상대에게 위임하게 된다.

테스트 시에는 이런 역할을 수행할 의존성 대상을 직접 생성할 수도 있지만, 의존성 주입 프레임워크인 Hilt를 사용하여 좀 더 쉽고 빠르게 의존성을 주입하여 테스트를 간편화 할 수 있다.

이번 글에서는 테스트 시 Hilt를 적용하는 방법 중 간단한 내용에 대해 정리하고자 한다. 그 후 Hilt를 이용한 Testing 시 필요한 지식과 다음 글에서 Hilt를 이용한 복잡한 UI(Activity, Fragment 등)를 쉽게 테스트 하는 방법에 대해서도 정리한다.

 

테스트 저장소

테스트 코드를 모두 작성한 내용은 아래 저장소를 참고하길 바란다. 브랜치는 testing_with_hilt 이다.

https://github.com/linuxias/Android-Testing/tree/testing_with_hilt/Setup_For_Testing/app/src/androidTest/java/com/linuxias/setup_for_testing

 

GitHub - linuxias/Android-Testing

Contribute to linuxias/Android-Testing development by creating an account on GitHub.

github.com

 

필요한 의존성 라이브러리

테스트 시에는 다양한 라이브러리가 필요하지만 Dagger-Hilt를 테스트에 이용시에 필요한 의존성을 아래와 같다. 아래의 의존성을 build.gradle (App 레벨)에 추가해 준다.

androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44'

(2022.11.08 현재 기준으로 버전 2.44가 최신 버전이다.)

 

 

테스트 대상과 목적

테스트 하려는 대상은 Dao (Data Access Object) 이다. Dao를 얻으려면 Database에 의존성을 필요로 한다. 따라서 Hilt를 이용하여 Dao에 Database (InMemory) 객체의 의존성을 주입해주려 한다.

처음 Hilt를 사용하지 않은 테스트의 @Before를 확인해보자. database를 Room을 이용하여 인메모리 데이터베이스를 생성하여 database 인스턴스를 생성한다. dao는 생성된 database에서 얻어오게 된다.

우리는 이 database를 Hilt를 이용하여 DI하는 방식으로 변경할 것이다.

@ExperimentalCoroutinesApi
@SmallTest
class MarsPhotoDaoTest {
    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()

    private lateinit var database: MarsPhotoItemDatabase
    private lateinit var dao: MarsPhotoDao

    @Before
    fun setup() {
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            MarsPhotoItemDatabase::class.java
        ).allowMainThreadQueries().build()
        dao = database.marsPhotoDao()
    }
    ...
}

 

테스트에서 Hilt를 사용하기 위해서

테스트에서 Hilt를 사용하기 위해서 아래와 같은 요소가 필수이다.

  1. Test에 @HiltAndroidTest, 애노테이션을 추가한다.
  2. 테스트 룰에 HiltAndroidRule 을 추가한다.
  3. 안드로이드 어플리케이션 클래스를 위해 HiltTestApplication 을 사용한다.

이 요소들은 설명을 하면서 하나씩 추가를 할 것이다.

 

의존성 주입 대상과 테스트 시 어떻게?

Room을 이용한 Database 객체 생성 시 프로젝트의 사용하던 모듈이 있을 것이다. 해당 모듈의 코드를 먼저 살펴본다. DatabaseModule이란 이름으로 MarsPhotoItemDatabase 객체를 생성하여 주입할 수 있도록 모듈화 되어 있다.

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Singleton
    @Provides
    fun provideMarsPhotoItemDatabase(
        @ApplicationContext context: Context
    ) = Room.databaseBuilder(context, MarsPhotoItemDatabase::class.java, DATABASE_NAME).build()
}

그렇다면 테스트 시 필요한 모듈 코드는 어떤지 아래 코드를 보자. @Module 애노테이션은 익숙할테니 설명은 스킵하고, TestDatabaseModule 오브젝트를 살펴보자. 기존 코드는 Room을 이용하여 Database를 생성하는데, 테스트 시에는 인메모리 데이터베이스를 생성하여 테스트를 진행한다.

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [DatabaseModule::class]
)
object TestDatabaseModule {
    @Provides
    fun provideInMemoryDb(@ApplicationContext context: Context) =
        Room.inMemoryDatabaseBuilder(context, MarsPhotoItemDatabase::class.java)
            .allowMainThreadQueries()
            .build()
}

@TestInstallIn 애노테이션?

기존 코드와 다른 점은 @InstallIn 대신 @TestInstallIn를 사용하였다.  애노테이션의 구성요소는 아래와 같다. 특히 replaces를 주목하자.

components
Returns the component(s) into which the annotated module will be installed.
replaces
Returns the InstallIn module(s) that the annotated class will replace in tests.

@TestInstallIn으로 주석이 달린 Dagger 모듈을 통해 사용자가 지정된 소스 집합의 모든 테스트에 대한 기존 @InstallIn 모듈. 예를들면, 위처럼 DatabaseModule을 TestDatabaseModule로 대체한다. TestDatabaseModule에 주석을 달면 이 작업을 수행할 수 있습니다.

즉 MarsPhotoItemDatabase의 인스턴스를 DatabaseModule이 아닌 TestDatabaseModule에 의해 주입되어야 하므로 @TestInstallIn 애노테이션을 사용한다.

 

 

Hilt Application 정의하기.

Hilt를 안드로이드 프로젝트에 적용하기 위해서 Hilt를 사용하는 모든 앱은 @HiltAndroidApp으로 주석이 지정된 어플리케이션 클래스를 포함해야 한다.

@HiltAndroidApp은 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt의 코드 생성을 트리거한다. 

테스트에서는 @HiltAndroidApp 애노테이션을 사용할 수는 없다. 그래서 아래와 같은 방식으로 AndroidJUnitRunner를 상속받은 클래스에서 newApplication() 함수를 오버라이드한다.

오버라이드 한 함수에서 super.newApplication에서 HiltTestApplication을 전달함으로써 @HiltAndroidApp과 동일한 역할을 수행할 수 있도록 한다.

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

추가로 이 어플리케이션이 Test를 위함임을 build.gradle에 아래와 같이 명시해야 한다. 명시하는 위치는 defaultConfig 내부에 testInstrumentationRunner에 설정한다.

defaultConfig {
    applicationId "com.linuxias.setup_for_testing"
    minSdk 30
    targetSdk 32
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "com.linuxias.setup_for_testing.HiltTestRunner"
}

 

테스트 코드

@ExperimentalCoroutinesApi
@SmallTest
@HiltAndroidTest
class MarsPhotoDaoTest {
    @get:Rule(order = 0)
    var hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    var instantTaskExecutorRule = InstantTaskExecutorRule()

    @Inject
    lateinit var database: MarsPhotoItemDatabase
    private lateinit var dao: MarsPhotoDao

    @Before
    fun setup() {
        /*
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            MarsPhotoItemDatabase::class.java
        ).allowMainThreadQueries().build()
        */
        hiltRule.inject()
        dao = database.marsPhotoDao()
    }

    @After
    fun teardown() {
        database.close()
    }

    @Test
    fun insertTestEntity() = runTest {
        val testEntity = MarsPhotoItem(1000, "id", "type", "resource_link", 1)
        dao.insertMarsPhotoItem(testEntity)

        val allTestEntities = dao.observeAllMarsPhotoItems().getOrAwaitValue()
        assertThat(allTestEntities).contains(testEntity)
    }
}

@HiltAndroidTest

@HiltAndroidTest와 함께 Hilt를 사용하는 테스트에 애노테이션을 지정해야 한다. @HiltAndroidTest 애노테이션은 각 테스트에 관한 Hilt 구성요소 생성을 담당한다.

@get:Rule - HiltAndroidRule

HiltAndroidRule은 구성요소의 상태를 관리하고, 테스트에서 삽입을 실행하는 데 사용된다. @Before 를 살펴보면, hiltRule.inject() 를 호출하는데, 이 함수로 인해 의존성 주입이 실제로 실행된다. 따라서 인메모리 데이터베이스를 만들지 않고, hiltRule.inject()를 통해 database에 의존성이 주입되게 된다.

    @Before
    fun setup() {
        /*
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            MarsPhotoItemDatabase::class.java
        ).allowMainThreadQueries().build()
        */
        hiltRule.inject()
        dao = database.marsPhotoDao()
    }

 

추가로 필요한 사항 (LiveDataTestUtil)

LiveDataTestUtil의 필요성과 코드 설명은 앞 글에서 몇 번 다뤘기에 여기서는 코드만 첨부한다.

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

 

Reference

https://dagger.dev/hilt/testing
https://developer.android.com/training/dependency-injection/hilt-testing

반응형
저작자표시 비영리 (새창열림)

'Android > Testing' 카테고리의 다른 글

[Android/Testing] #10. Fragment Navigation Test  (0) 2022.11.18
[Android/Testing] #9. Dagger-Hilt 적용하여 프레그먼트 테스트하기  (0) 2022.11.13
[Android/Testing] #7. ViewModel 테스트하기 (with Fake Repository)  (0) 2022.10.22
[Android/Testing] #6. 테스트 시작 전에 테스트 더블(Test Double) 이해하기  (0) 2022.10.08
[Android/Testing] #5. 테스트를 위한 기반 프로젝트 생성하기  (0) 2022.10.02
    'Android/Testing' 카테고리의 다른 글
    • [Android/Testing] #10. Fragment Navigation Test
    • [Android/Testing] #9. Dagger-Hilt 적용하여 프레그먼트 테스트하기
    • [Android/Testing] #7. ViewModel 테스트하기 (with Fake Repository)
    • [Android/Testing] #6. 테스트 시작 전에 테스트 더블(Test Double) 이해하기
    Linuxias
    Linuxias
    I want to be a S/W developer who benefits people.

    티스토리툴바