Jetpack Compose - Navigation, Instrumentation Test

2024. 2. 7. 16:01IT/Android

728x90

<미리 보기>

 

 

 

<소스 코드>

https://github.com/SeongHyunJeon/android-kotlin-practice/tree/991692b129d3287e9960bdf06765b32f330c0192/Cupcake

 

 

 

<정리>

Navigation 구성 요소.

1) NavController - 대상(화면) 간 이동을 담당한다.

2) NavGraph - 화면 간 이동의 경로 설정을 담당한다.

3) NavHost - NavController, NavGraph를 근거하여 화면 간의 전환, 데이터 전달, 백 스택 처리들을 편리하게 처리할 수 있는 탐색 구조가 명확한 컴포저블. 


경로 설정 - 각각의 화면을 나타내는 컴포지션들에 URL처럼 고유한 문자열 경로를 지정해줄 수 있는데, enum 클래스를 사용하면 가독성이 좋아지고 오타로 인한 버그를 방지할 수 있다.

enum class CupcakeScreen(@StringRes val title: Int) {
    Start(title = R.string.app_name),
    Flavor(title = R.string.choose_flavor),
    Pickup(title = R.string.choose_pickup_date),
    Summary(title = R.string.order_summary)
}

 


NavHost 컴포저블의 대략적인 구조.

NavHost(
    navController = /* 화면 이동에 사용할 탐색 컨트롤러 */
    startDestination = /* 처음 화면에 보여질 경로 */
    modifier = Modifier
) {
    //각각의 화면은 composable()로 정의한다. 따라서 화면이 4개면 총 composable() 함수 4개가 존재.
    composable(route = /* 해당 화면에 대한 경로 */) {
    	/* 하나의 화면이 될 구성 가능한 함수 */
    }
}

 

rememberNavController() - 탐색 컨트롤러 NavController의 서브 클래스 NavHostController 인스턴스를 생성한다.

 

NavController.navigate() - 파라미터로 전달된 경로(route)로 화면이 이동된다.

 

백 스택(Back Stack) - 사용자가 앱과 상호작용을 하면서 화면을 이동하면 해당 경로들은 처음 화면의 경로 위에 켜켜히 쌓이게 되는데 이렇게 쌓인 경로들을 백 스택이라 부른다. 참고로 뒤로 가기 버튼을 누르면 이전 화면으로의 이동과 동시에 백 스택 가장 상단에 위치한 현재 화면의 경로는 삭제된다.

 

NavController.popBackStack() - 첫 번째 파라미터(route)는 돌아갈 경로, 두 번째 파라미터(inclusive)는 불리언 값을 받는다.

1) 두 번째 파라미터(inclusive)가 true인 경우 - 첫 번째 파라미터로 받은 경로를 포함하여 백 스택을 비운다.

2) 두 번째 파라미터(inclusive)가 false인 경우 - 첫 번째 파라미터로 받은 경로를 제외하고 백 스택을 비운다.

*만약 첫 번째 파라미터로 첫 화면의 경로를 전달하고, 두 번째 파라미터로 true를 전달하면 백 스택에 어떤 경로도 남아 있지 않아 오류가 발생할 수 있으니 주의해야 한다. 예를 들어 백스택 상태가 **[A, B, C]**인 상황에서 popBackStack(A, true)를 실행하면 백스택에 아무것도 남지 않게 되어 빈화면을 출력한다.

 

NavController.currentBackStackEntryAsState() - 백 스택 가장 상단에 위치한 현재 화면의 경로를 추적하는 메서드.

 

NavController.previousBackStackEntry - 현재 화면을 기준으로 직전 백 스택 항목만을 반환하는 프로퍼티.

 

NavController.navigateUp() - 이전 화면으로 이동하는 메서드로, 예를 들어 백스택 상태가 **[A, B, C]**인 상황에서 navigateUp()을 실행하면 백스택 상태는 **[A, B]**가 되고 화면 B를 출력하게 된다. 만약 백스택에 상위 경로 없이 단 하나의 경로만 존재한다면 동작하지 않는 특징이 있다.


인텐트(Intent) - 액티비티, 브로드캐스트 리시버, 서비스 같은 앱 구성 요소 간 통신을 용이하게 하고 다양한 앱 컴포넌트 간의 상호작용을 가능하게 하는 메시지 객체.

 

private fun shareOrder(context: Context, subject: String, summary: String) {
    val intent = Intent(Intent.ACTION_SEND).apply { //데이터를 담아 공유할 수 있는 인텐트 생성.
        type = "text/plain" //인텐트와 함께 전달되는 데이터 유형.
        putExtra(Intent.EXTRA_SUBJECT, subject) //제목이 되는 데이터를 키와 함께 인텐트에 추가.
        putExtra(Intent.EXTRA_TEXT, summary) //본문이 되는 데이터를 키와 함께 인텐트에 추가.
    }
    context.startActivity( //해당 컨텍스트에서 새로운 액티비티를 실행.
        Intent.createChooser( //해당 액티비티에서 실행될 인텐트 설정.
            intent,
            context.getString(R.string.new_cupcake_order)
        )
    )
}

plurals 리소스 - 특정 양에 대해 다양한 문자열 표현을 제공할 수 있는 방법으로 해당 범위에 적합한 문자열이 반환된다.

<resources>
    <plurals name="cupcakes">
        <item quantity="one">%d cupcake</item> //1
        <item quantity="few">%d cupcakes</item> //2~4
        <item quantity="many">%d cupcakes</item> //5~10
        <item quantity="other">%d cupcakes</item> //그 외.
    </plurals>
</resources>
val numberOfCupcakes = resources.getQuantityString(
    R.plurals.cupcakes,
    orderUiState.quantity,
    orderUiState.quantity
    orderUiState.quantity
    orderUiState.quantity
)

계측 테스트(Instrumentation Test)

 

 

1) 탐색 테스트 - 앱과 상호 작용을 통한 탐색 경로가 일치하는지 확인.

 

*@Before - 해당 주석을 설정한 메서드는 테스트 메서드(@Test) 보다 먼저 실행되므로 초기화 같은 반복적인 코드를 적어두는 것이 좋다. 각각의 테스트 메서드(@Test)는 독립적으로 실행되기 때문에, 항상 @Before 주석이 붙은 메서드가 실행된 상태에서 시작된다.

@Before
fun setupCupcakeNavHost() {
    composeTestRule.setContent { //테스트 하기 위한 초기화 작업, 컴포지션 설정.
        navController = TestNavHostController(LocalContext.current).apply {
            navigatorProvider.addNavigator(ComposeNavigator())
        } //TestNavHostController에는 기본 네비게이터를 추가해야 사용할 수 있다.
        CupcakeApp(navController = navController)
    }
}
@Test
fun cupcakeNavHost_verifyStartDestination() {
    navController.assertCurrentRouteName(CupcakeScreen.Start.name)
} //처음 시작 화면의 경로를 확인하는 메서드.

@Test
fun cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen() {
    val backText = composeTestRule.activity.getString(R.string.back_button)
    composeTestRule.onNodeWithContentDescription(backText).assertDoesNotExist()
} //처음 시작 화면에서 위로 버튼이 없음을 확인하는 메서드.

...

 

2) 단일 화면 테스트 - 해당 화면의 UI 요소와 그 속성이 설정과 일치하는지 확인.

@Test
fun selectOptionScreen_verifyContent() {
    val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
    val subtotal = "$100"

    composeTestRule.setContent {
        SelectOptionScreen(subtotal = subtotal, options = flavors)
    }

    flavors.forEach { flavor ->
        composeTestRule.onNodeWithText(flavor).assertIsDisplayed()
    }

    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(
            R.string.subtotal_price,
            subtotal
        )
    ).assertIsDisplayed()

    composeTestRule.onNodeWithStringId(R.string.next).assertIsNotEnabled()
}
728x90