https://dagger.dev/hilt/testing


6.1 Testing – Testing 개요

소개

Note : 현재 Hilt는 안드로이드 Instrumentation 과 Robolectric 테스트만 지원한다. Hilt는 vanilla JVM 테스트에서는 사용할 수 없지만 평소와 같이 이러한 테스트를 작성하지 못하게 막지는 않는다.

Hilt는 안드로이드 테스트에 의존성 주입의 기능을 제공하여 테스트를 더 쉽게 만든다. Hilt는 테스트를 통해 Dagger 바인딩에 쉽게 접근하거나 새로운 바인딩을 제공하여 바인딩을 대체 할 수 있다. 각 테스트마다 Hilt 컴포넌트 세트를 가져오므로 테스트 레벨별로 바인딩을 쉽게 사용자화 할 수 있다.

이 문서에 설명된 많은 테스트 API들과 기능은 훌륭한 테스트를 수행하는 것에 대한 무언의 철학을 기반으로 하고 있다. Hilt의 테스트 철학에 대한 자세한 내용은 ‘10.2 테스트 철학’을 참고하자.

테스트 설정하기

Note : Gradle 사용자는 ‘3. Gradle 설정하기’ 편을 참고하여 Hilt 테스트 빌드 의존성을 먼저 추가해야 한다.

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

  1. 테스트 클래스에 @HiltAndroidTest을 추가한다.
  2. HiltAndroidRule 테스트 룰을 추가한다.
  3. 안드로이드 Application 클래스를 위해 HiltTestApplication을 사용한다.

예를 들면,

@HiltAndroidTest
class FooTest {
    @get:Rule val rule = HiltAndroidRule(this)
    ...
}

앞에서 언급한 3번째에서, 테스트를 위한 Application 클래스를 설정하는 것은 테스트가 Robolectric 테스트인지, Instrumentation 테스트인지에 따라 다르다. 자세한 내용은 ‘6.2 Robolectric 테스트하기’ 또는 ‘6.3 Instrumentation 테스트하기’를 참고하자. 이 장에서 다루는 내용은 Robolectric 그리고 Instrumentation 테스트 둘다 적용 가능한 부분을 다룬다.

바인딩에 접근하기

테스트는 종종 Hilt 컴포넌트로부터 바인딩을 요청하게 된다. 이 섹션에서는 각기 다른 컴포넌트의 바인딩을 어떻게 요청하는지 살펴본다.

ApplicationComponent 바인딩에 접근하기

ApplicationComponent 바인딩은 @Inject를 사용하여 필드에 직접적으로 주입이 가능하다. HiltAndroidRule#inject()를 호출하기 전까지 주입은 일어나지 않는다.

@HiltAndroidTest
class FooTest {

    @get:Rule HiltAndroidRule hiltRule = HiltAndroidRule(this)

    @Inject foo: Foo@Test

    fun testFoo() {
        assertThat(foo).isNull()
        hiltRule.inject()
        assertThat(foo).isNotNull()
    }
}

ActivityComponent 바인딩에 접근하기

ActivityComponent 바인딩을 요청하는 것은 Hilt를 사용하는 Activity의 인스턴스를 필요로 한다. 한가지 방법은 작성한 테스트에 필요한 바인딩에 대해 @Inject 필드를 포함하는 내재된 Activity를 정의하는 것이다. 그런 다음에 테스트 액티비티 인스턴스를 생성하여 바인딩을 얻을 수 있다.

@HiltAndroidTest
class FooTest {

    @AndroidEntryPoint
    class TestActivity : AppCompatActivity() {
        @Inject foo: Foo
    }

    ...
    val foo = testActivity.foo
}

다른 방법으로, 테스트에서 Hilt를 사용하는 Activity 인스턴스를 가지고 있다면, EntryPoint를 사용하여 어떤 ActivityComponent 바인딩도 얻을 수 있게 된다.

@HiltAndroidTest
class FooTest {

    @EntryPoint
    @InstallIn(ActivityComponent::class)
    interface FooEntryPoint {
        fun getFoo() : Foo
    }

    ...
    val foo = EntryPoints.get(activity, FooEntryPoint::class.java).getFoo()
}

FragmentComponent 바인딩 접근하기

ActivityComponent 바인딩에 접근하는 방법과 유사하게 FragmentComponent 바인딩에 접근할 수 있다. 주요 차이점은 FragmentComponent 바인딩에 접근하는 것은 Hilt를 사용하는 Activity와 Fragment 인스턴스를 모두 필요로 한다는 점이다.

@HiltAndroidTest
class FooTest {

    @AndroidEntryPoint
    class TestFragment : Fragment() {
      @Inject foo: Foo
    }

    ...
    val foo = testFragment.foo
}

다른 방법으로, 테스트에서 Hilt를 사용하는 Fragment 인스턴스를 가지고 있다면, EntryPoint를 사용하여 어떤 FragmentComponent 바인딩도 얻을 수 있게 된다.

@HiltAndroidTest
class FooTest {

    @EntryPoint
    @InstallIn(FragmentComponent::class)
    interface FooEntryPoint {
        fun getFoo() : Foo
    }

    ...
    val foo = EntryPoints.get(fragment, FooEntryPoint::class.java).getFoo()
}
Warning : Hilt는 Activity 클래스를 지정할 방법이 없고 Hilt를 사용하는 Fragment는 Hilt를 사용하는 Activity에 포함되어야 하기 때문에 현재는 FragmentScenario를 지원하고 있지 않다. 한 가지 해결책은 Hilt를 사용하는 Activity를 실행하고 Fragment를 붙이는 것이다.

바인딩 추가하기

테스트는 애플리케이션의 프로덕션 빌드에 포함되지 않은 추가 Dagger 바인딩을 추가적으로 제공해야 할 수도 있다. 또한 테스트들은 테스트마다 다른 값으로 동일한 바인딩을 제공해야 할 수도 있다. 이 섹션에서는 Hilt를 사용하여 테스트에 바인딩을 제공하는 방법에 대해 설명한다.

내재된 모듈

보통 @InstallIn 모듈은 모든 테스트의 Hilt 컴포넌트에서 설치된다. 하지만 바인딩이 특정 테스트에서만 설치되어야 한다면 테스트 클래스내에 @InstallIn 모듈을 내재 시켜서 수행 할 수 있다.

@HiltAndroidTest
class FooTest {

    // 내재된 모듈은 테스트 밖의 Hilt 컴포넌트에서만 설치된다
    @Module
    @InstallIn(ApplicationComponent::class)
    object FakeBarModule {
        @Provides 
        fun provideBar() = Bar()
    }
    ...
}

따라서 다른 구현으로 동일한 바인딩을 제공해야 하는 다른 테스트가 있는 경우 중복 바인딩 충돌없이 수행 할 수 있다

Hilt는 정적으로 내재된 @InstallIn 모듈 외에도 테스트 내에서 내부 (비-정적) @InstallIn 모듈을 지원한다. 내부 모듈을 사용하면 @Provides 메소드가 테스트 인스턴스의 멤버를 참조 할 수 있다.

Warning : Hilt는 생성자 매개 변수가 있는 @InstallIn 모듈을 지원하지 않는다.

@BindValue

간단한 바인딩, 특히 테스트 메서드에서 접근해야 하는 바인딩의 경우, 모듈과 바인딩을 프로비전 메서드를 생성하는 보일러플레이트 코드를 회피하기 위해 Hilt는 편리한 어노테이션을 제공한다.

@BindValue는 쉽게 테스트에서 필드를 Dagger 그래프에 바인딩 할 수 있는 어노테이션이다. 이를 사용하기 위해, @BindValue 어노테이션을 필드에 추가하면 선언된 한정자 그리고 필드 타입과 함께 바인딩 된다.

@HiltAndroidTest
class FooTest {
    ...
    @BindValue fakeBar: Bar = new FakeBar()
}

바인딩의 스코프가 필드에 연결되고 테스트에 의해 제어되므로 @BindValue는 스코프 어노테이션의 사용을 지원하지 않는다. 필드 값은 요청 될 때마다 쿼리되므로 테스트에서 필요한 대로 변경 될 수 있다. 바인딩을 실질적으로 싱글톤으로 만드려면 필드가 테스트당 한번만 설정되도록 해야한다. 예를들어 필드의 이니셜라이저 또는 테스트의 @Before 메소드에서 필드 값을 설정한다.

마찬가지로 Hilt에는 @IntoSet, @ElementIntoSet 및 @IntoMap을 각각 지원하기 위해 @BindValueIntoSet, @BindElementsIntoSet 및 @BindValueIntoMap을 이용한 멀티 바인딩에 대한 편리한 어노테이션도 있다. (@BindValueIntoMap을 사용하려면 필드에 @MapKey 어노테이션을 추가해야 한다.)

주의사항

@BindValue를 사용하거나 비-정적인 내부 모듈과 함께 ActivityScenarioRule을 사용할 때는 주의해야 한다. ActivityScenarioRule은 @Before 메서드를 호출하기 전에 Activity를 생성하므로, @BindValue 필드가 @Before(또는 그 이후)에 초기화 된다면 필드가 초기화 되지 않은 상태에서 바인딩을 Activity에 주입할 가능성이 있다. 이런 경우를 피하려면 @BindValue를 필드에서 바로 초기화 하면 된다.

바인딩 교체하기

@UninstallModules

일반적인 테스트 유즈 케이스에서는 프로덕션용 바인딩을 테스트용 바인딩으로 교체할 필요성이 있다. 예를 들면 Fake 또는 Mock이 있는데, Hilt 테스트에서 프로덕션용 바이딩은 @UninstallModules를 사용하여 포함된 프로덕션 모듈을 먼저 제거하여 프로덕션 바인딩을 교체 할 수 있다. 그런 뒤 테스트에서 새로운 바인딩을 제공한다.

@UninstallModules(ProdFooModule::class)
@HiltAndroidTest
class FooTest {

  ...
  @BindValue fakeFoo: Foo = new FakeFoo()
}

@UninstallModules는 주어진 테스트와 관련하여 해당 모듈에서 @InstallIn 어노테이션을 제거하는 것과 같다. Hilt는 개별 바인딩 제거를 직접 지원하지 않지만 특정 모듈에 단일 바인딩만 포함하면 실질적으로 가능하다.

사용자화 테스트 애플리케이션

모든 Hilt 테스트는 반드시 안드로이드 Application 클래스 대용으로 HiltTestApplication을 사용해야 한다. Hilt는 기본 테스트용 Application인 HiltTestApplication을 제공한다. HiltTestApplication은 MultiDexApplication을 확장한 클래스다. 그러나 다른 기본 클래스를 사용해야 해서 테스트하기 힘든 몇가지 경우가 있다.

@CustomTestApplication

만약 테스트에서 사용자화 기본 클래스가 필요한 경우에는 @CustomTestApplication 어노테이션을 통해 주어진 Application클래스를 확장하는 Hilt용 테스트 애플리케이션을 생성할 수 있다.

@CustomTestApplication을 사용하려면 클래스 또는 인터페이스에 @CustomTestApplication 어노테이션을 추가하고 어노테이션 값으로 기본 클래스를 명시하면 된다.

// MyCustom_Application.class를 생성하게 된다.
@CustomTestApplication(MyBaseApplication::class)
interface MyCustom

앞의 예제는 Hilt가 MyCustom_Application이라는 MyBaseApplication을 확장한 클래스를 생성한다. 일반적으로 생성된 Application 이름은 어노테이션이 달린 클래스의 이름 뒤에 _Application을 붙인다. 어노테이션이 달린 클래스가 내재된 클래스라면 클래스의 이름은 바깥 클래스의 이름도 밑줄표시로 분리하여 포함하게 된다. 어노테이션이 달린 클래스는 생성된 Application의 이름 외에는 관련이 없다.

모범 사례

모범 사례로 @CustomTestApplication 사용을 피하고 대신에 테스트에서 HiltTestApplication을 사용하자. 일반적으로 Activity, Fragment 등이 포함된 상위 클래스와 독립적으로 만들면 나중에 쉽게 구성하고 재사용 할 수 있다.

하지만 사용자화 기본 Application을 반드시 사용해야 한다면 프로덕션 생명주기에 대해 알아야 할 몇가지 미묘한 차이점이 있다.

첫번째 차이점은 Instrumentation 테스트는 같은 Application 인스턴스를 매 테스트마다 사용하게 된다. 그러므로 사용자화 테스트 Application을 사용할 때는 테스트 전반에 걸쳐 뜻하지 않은 구멍이 생기기 마련이다. Application 상태에 의존적이거나 상태를 저장하는 테스트를 만들지 않는 것이 중요하다.

또 다른 차이점은 테스트 Application에서 Hilt 컴포넌트는 super#onCreate에서 생성되지 않는다. 이런 제약은 주로 Hilt의 일부 기능이(예: @BindValue) 테스트 인스턴스에 의존하기 때문에 Application#onCreate가 호출 될 때까지 테스트에서 사용할 수 없기 때문에 발생한다. 따라서 프로덕션용 Application과 달리 사용자화 기본 Application은 Application#onCreate 중에 컴포넌트를 호출하지 않아야 한다. 여기에는 Application을 멤버로 주입하는 것이 포함된다. 이런 문제를 방지하기 위해서 Hilt는 기본 Application에서 의존성 주입하는 것을 허용하고 있지 않다.

Hilt 규칙 순서

여러가지 테스트 규칙을 사용한다면 HiltAndroidRule이 Hilt 컴포넌트에 접근을 필요로하는 다른 테스트 규칙 전에 실행되도록 해야 한다. 예를 들면 ActivityScenarioRule은 Activity#onCreate를 호출한다(Hilt를 사용하는 Activity의 경우 Hilt 컴포넌트가 의존성 주입을 수행해야 한다). 따라서 ActivityScenarioRule은 컴포넌트가 적절히 초기화 된 것을 보장하기 위해 HiltAndroidRule 후에 실행되어야 한다.

Note : 만약 4.13 버전 미만의 JUnit을 사용한다면 RuleChain을 사용하여 순서를 명시하자.
@HiltAndroidTest
class FooTest {
    // ActivityScenarioRule을 실행하기 전에 Hilt 컴포넌트가 초기화되었는지 확인한다.
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this);

    @get:Rule(order = 1)
    val scenarioRule = ActivityScenarioRule(MyActivity::class.java)
}
카테고리: Dagger2

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.