[Android] 클린 아키텍처로 hilt + Compose UI 계측 테스트 하기

sundays·2023년 4월 21일
0

cleanArchitecture

목록 보기
5/6

이번에는 Compose UI를 테스트 하기위해 모든 UI테스트의 근간이 되는 mockk데이터를 UI에서 입력해서 넣어보는 테스트를 할 것입니다. 이전처럼 fakeRepository를 사용하지 않고 Test코드를 이전에 사용했던 Hilt를 적용하여 데이터를 넣을 것입니다. 보통 이러한 테스트를 계측 테스트라고 하며 이것은 실제 장치나 에뮬레이터에서 실행되어야 하기 때문에 build.gradle에 선언하여 사용됩니다.

Dependencies

테스트에서 hilt를 사용하기 위해서는 다음과 같은 dependency들이 필요 합니다.

dependencies {
    // For Robolectric tests.
    testImplementation("com.google.dagger:hilt-android-testing:2.44")
    // ...with Kotlin.
    kaptTest("com.google.dagger:hilt-android-compiler:2.44")
    // ...with Java.
    testAnnotationProcessor("com.google.dagger:hilt-android-compiler:2.44")

    // For instrumented tests.
    androidTestImplementation("com.google.dagger:hilt-android-testing:2.44")
    // ...with Kotlin.
    kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.44")
    // ...with Java.
    androidTestAnnotationProcessor("com.google.dagger:hilt-android-compiler:2.44")
}

HiltModule

먼저 기존의 Hilt DI들을 복사해서 같은 기능을 제공하게 하려고 합니다. 이 테스트에서의 용도는 Room Database 를 적용하기 위해 inMemoryDatabaseBuilder 를 사용하였습니다. 이것은 프로세스가 종료되면 사라지게될 메모리 형태의 데이터베이스 입니다. 기존의 DB접근 방법과 다른 것은 이것뿐입니다.

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

    @Provides
    @Singleton
    fun provideNoteDatabase(app: Application): NoteDatabase {
        return Room.inMemoryDatabaseBuilder(
            app,
            NoteDatabase::class.java,
        ).build()
    }
    ...
}

HiltAndroidTest

hiltAndroidTest 를 사용해서 Hilt를 사용할 UI 테스트를 사용할때에는 HiltAndroidRule(this) 를 사용하여 현재 컨텍스트에서 구성요소의 상태를 관리할 수 있게 됩니다.

@HiltAndroidTest
class NotesScreenTest {

  @get:Rule
  var hiltRule = HiltAndroidRule(this)

  // UI tests here.
}

여기서 기존의 사용하던 DI를 모두 지우고 새로운 테스트에 사용할 DI를 입력하면 됩니다. 그러기 위해 사용하는것이 @UninstallModules 입니다.

@HiltAndroidTest
@UninstallModules(AppModule::class) // 기존 HILT DI 클래스명
class NotesScreenTest { .. }

HiltTestApplication

테스트 어플리케이션을 선언해야 합니다. AndroidJUnitRunner() 를 확장하여 newApplication에 HiltTestApplication을 사용해 줍니다. (이것은 TestApp을 적용할때 같은 방식으로 적용됩니다.)

CustomTestRunner

class CustomTestRunner : AndroidJUnitRunner() {

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

build.gradle

계측 테스트가 시작 되는 부분의 패키지와 클래스명을 build.android.defaultConfig.testInstrumentationRunnder 에 입력하여 선언해야 합니다. 이렇게 하면 계측 테스트를 사용할 환경이 완료 됩니다.

android {
    defaultConfig {
        // Replace com.example.android.dagger with your class path.
        testInstrumentationRunner = "com.example.android.dagger.CustomTestRunner"
    }
}

ComposeTestRule

본격적으로 테스트 코드를 작성하기 전에 composeTestRule 은 전체 애플리케이션이나 단일화면 작은 요소등 composable acitivity를 시작할 수 있습니다. 이것을 사용하기 위해서는 gradle에 dependency를 추가합니다

build.gradle

debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

테스트 코드 (SetUp)

테스트 환경을 먼저 셋팅하기 위해 @Before 에서 초기 상태를 설정합니다. 저의 경우에는 NoteScreen(navController = navController) 을 생성하기 위해 composable를 선언 해주었습니다.

@HiltAndroidTest
@UninstallModules(AppModule::class)
class NotesScreenTest {

    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)
	
    @get:Rule(order = 1)
    val composeRule = createAndroidComposeRule<MainActivity>()

    @ExperimentalAnimationApi
    @Before
    fun setUp() {
        hiltRule.inject()
        composeRule.activity.setContent {
            val navController = rememberNavController()
            NoteAppTheme {
                NavHost(
                    navController = navController,
                    startDestination = Screen.NoteScreen.route
                ) {
                    composable(route = Screen.NoteScreen.route) {
                        NoteScreen(navController = navController)
                    }
                }
            }
        }
    }
    ...
}

createAndroidComposeRule 를 선언하면 compose가 실제 액티비티(코드상 MainActivity)의 리소스에도 액세스가 가능하게 되어 테스트 화면을 구성할 수 있게 됩니다.

테스트 코드 (Test)

드디어 실제로 테스트 코드를 작성합니다. composeRule를 사용해서 어떤 데이터를 입력할지, 어떤 컴포넌트를 작동할 것인지 순서대로 시나리오를 적듯이 작성하게 됩니다. 예를들면 다음과 같습니다.

	@Test
    fun clickToggleOrderSection_isVisible() {
    	// Modifier.testTag = TestTags.ORDER_SECTION 이 보이지 않는지 Assert 
        composeRule.onNodeWithTag(TestTags.ORDER_SECTION).assertDoesNotExist()
        // contentDescription = "Sort" 인 항목을 클릭한다
        composeRule.onNodeWithContentDescription("Sort").performClick()
        // Modifier.testTag = TestTags.ORDER_SECTION 이 보이고 있는지 Assert
        composeRule.onNodeWithTag(TestTags.ORDER_SECTION).assertIsDisplayed()
    }

실제 Compose UI가 구성된 항목을 살펴보자면 다음과 같습니다

...
Icon(imageVector = Icons.Default.Sort, contentDescription = "Sort")
...
OrderSection(
	noteOrder = state.noteOrder,
	onOrderChange = { viewModel.onEvent(NotesEvent.Order(it)) },
	modifier = Modifier
		.fillMaxWidth()
		.padding(vertical = 16.dp)
		.testTag(TestTags.ORDER_SECTION)
)

이미지 아이콘에 contentDescription = Sort이 선언된 해당 아이콘이 클릭되면 OrderSection에 있는 testTag의 내용이 노출되게 설계된 시나리오대로 작성된 테스트 입니다.
테스트에 존재하는 assert항목들이 전부 만족되고 나서 한개의 유닛 테스트가 종료 됩니다.

결론

계측 테스트를 드디어 작성하였습니다. 사실 클린 코드는 DI를 적용하는 부분만 해당하고 나머지 부분은 테스트 코드를 작성하는데 필요한 환경을 제공하는데 더 집중하게 된 것 같습니다. 해당 NoteApp의 소스코드는 전부 github에 업로드가 되어있으니 구조를 확인하면서 보기에 적당할 것 같습니다. 해당 구조는 PhilippLackner 의 구성을 따랐습니다.

Reference

profile
develop life

0개의 댓글