Cupcake 앱 테스트

1. 소개

Compose를 사용하여 화면 간 이동 Codelab에서는 Jetpack Navigation Compose 구성요소를 사용하여 Compose 앱에 탐색을 추가하는 방법을 알아봤습니다.

Cupcake 앱에는 탐색할 수 있는 여러 화면과 사용자가 실행할 수 있는 다양한 작업이 있습니다. 이 앱을 통해 자동 테스트 기술을 연마할 수 있습니다. 이 Codelab에서는 Cupcake 앱의 UI 테스트를 여러 개 작성하고 테스트 적용 범위를 극대화하는 방법을 알아봅니다.

기본 요건

학습할 내용

  • Compose로 Jetpack Navigation 구성요소를 테스트합니다.
  • 각 UI 테스트의 일관된 UI 상태를 만듭니다.
  • 테스트용 도우미 함수를 만듭니다.

빌드할 항목

  • Cupcake 앱의 UI 테스트

필요한 항목

  • 최신 버전의 Android 스튜디오
  • 시작 코드를 다운로드하기 위한 인터넷 연결

2. 시작 코드 다운로드하기

  1. Android 스튜디오에서 basic-android-kotlin-compose-training-cupcake 폴더를 엽니다.
  2. Android 스튜디오에서 Cupcake 앱 코드를 엽니다.

3. UI 테스트를 위한 Cupcake 설정

androidTest 종속 항목 추가

Gradle 빌드 도구를 사용하면 특정 모듈의 종속 항목을 추가할 수 있습니다. 이 기능을 통해 종속 항목이 불필요하게 컴파일되는 것을 방지할 수 있습니다. 프로젝트에 종속 항목을 포함할 때의 implementation 구성에 관해서는 이미 살펴본 바 있습니다. 이 키워드를 사용하여 앱 모듈의 build.gradle.kts 파일에서 종속 항목을 가져왔습니다. implementation 키워드를 사용하면 이 모듈의 모든 소스 세트에서 종속 항목을 사용할 수 있습니다. 지금까지 main, test, androidTest 소스 세트를 사용해 봤습니다.

UI 테스트는 자체 소스 세트 androidTest에 포함되어 있습니다. 이 모듈에만 필요한 종속 항목은 앱 코드가 포함된 다른 모듈(예: main 모듈)을 위해 컴파일할 필요가 없습니다. UI 테스트에서만 사용되는 종속 항목을 추가할 때는 androidTestImplementation 키워드를 사용하여 앱 모듈의 build.gradle.kts 파일에 종속 항목을 선언합니다. 이렇게 하면 UI 테스트 종속 항목이 UI 테스트를 실행할 때만 컴파일됩니다.

UI 테스트를 작성하는 데 필요한 종속 항목을 추가하려면 다음 단계를 완료하세요.

  1. build.gradle.kts(Module :app) 파일을 엽니다.
  2. 다음 종속 항목을 파일의 dependencies 섹션에 추가합니다.
androidTestImplementation(platform("androidx.compose:compose-bom:2023.05.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.navigation:navigation-testing:2.6.0")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")

UI 테스트 디렉터리 만들기

  1. 프로젝트 뷰에서 src 디렉터리를 마우스 오른쪽 버튼으로 클릭하고 New > Directory를 선택합니다.

31f950996e51807b.png

  1. androidTest/java 옵션을 선택합니다.

e1e4d108c863ae27.png

테스트 패키지 만들기

  1. 프로젝트 창에서 androidTest/java 디렉터리를 마우스 오른쪽 버튼으로 클릭하고 New > Package를 선택합니다.

c5cdd7125e61bf22.png

  1. 패키지 이름을 com.example.cupcake.test로 지정합니다. b6cec61624467422.png

탐색 테스트 클래스 만들기

test 디렉터리에서 새 Kotlin 클래스 CupcakeScreenNavigationTest를 만듭니다.

8da2547b2ef1cc8e.png

4. 탐색 호스트 설정

이전 Codelab에서는 Compose의 UI 테스트에 Compose 테스트 규칙이 필요하다는 것을 배웠습니다. Jetpack Navigation을 테스트할 때도 마찬가지입니다. 그러나 탐색을 테스트하려면 Compose 테스트 규칙을 통한 추가 설정이 필요합니다.

Compose Navigation을 테스트할 때는 앱 코드에서처럼 동일한 NavHostController에 액세스할 수 없습니다. 그러나 TestNavHostController를 사용하고 이 탐색 컨트롤러로 테스트 규칙을 구성할 수 있습니다. 이 섹션에서는 탐색 테스트의 테스트 규칙을 구성하고 재사용하는 방법을 알아봅니다.

  1. CupcakeScreenNavigationTest.kt에서 createAndroidComposeRule을 사용하고 ComponentActivity를 유형 매개변수로 전달하여 테스트 규칙을 만듭니다.
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import org.junit.Rule

@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

앱이 올바른 위치로 이동하게 하려면 TestNavHostController 인스턴스를 참조하여 앱이 이동 작업을 실행할 때 탐색 호스트의 탐색 경로를 확인해야 합니다.

  1. TestNavHostController 인스턴스를 lateinit 변수로 인스턴스화합니다. Kotlin에서 lateinit 키워드는 객체가 선언된 후 초기화할 수 있는 속성을 선언하는 데 사용됩니다.
import androidx.navigation.testing.TestNavHostController

private lateinit var navController: TestNavHostController

이제 UI 테스트에 사용할 컴포저블을 지정합니다.

  1. setupCupcakeNavHost() 메서드를 만듭니다.
  2. setupCupcakeNavHost() 메서드에서, 직접 작성한 Compose 테스트 규칙의 setContent() 메서드를 호출합니다.
  3. setContent() 메서드에 전달된 람다 내에서 CupcakeApp() 컴포저블을 호출합니다.
import com.example.cupcake.CupcakeApp

fun setupCupcakeNavHost() {
    composeTestRule.setContent {
        CupcakeApp()
    }
}

이제 테스트 클래스에서 TestNavHostContoller 객체를 만들어야 합니다. 나중에 이 객체를 사용하여 탐색 상태를 확인합니다. 앱이 컨트롤러를 사용하여 Cupcake 앱의 다양한 화면을 이동하기 때문입니다.

  1. 이전에 만든 람다를 사용하여 탐색 호스트를 설정합니다. 직접 만든 navController 변수를 초기화하고 탐색기를 등록한 후 TestNavHostControllerCupcakeApp 컴포저블에 전달합니다.
import androidx.compose.ui.platform.LocalContext

fun setupCupcakeNavHost() {
    composeTestRule.setContent {
        navController = TestNavHostController(LocalContext.current).apply {
            navigatorProvider.addNavigator(ComposeNavigator())
        }
        CupcakeApp(navController = navController)
    }
}

CupcakeScreenNavigationTest 클래스의 모든 테스트에는 탐색의 측면을 테스트하는 작업이 포함됩니다. 따라서 각 테스트는 개발자가 만든 TestNavHostController 객체에 따라 달라집니다. 모든 테스트에서 setupCupcakeNavHost() 함수를 수동으로 호출하여 탐색 컨트롤러를 설정하는 대신 junit 라이브러리에서 제공하는 @Before 주석을 사용하여 자동으로 이 작업을 실행할 수 있습니다. 메서드가 @Before로 주석 처리되면 @Test 주석이 달린 모든 메서드보다 먼저 실행됩니다.

  1. setupCupcakeNavHost() 메서드에 @Before 주석을 추가합니다.
import org.junit.Before

@Before
fun setupCupcakeNavHost() {
    composeTestRule.setContent {
        navController = TestNavHostController(LocalContext.current).apply {
            navigatorProvider.addNavigator(ComposeNavigator())
        }
        CupcakeApp(navController = navController)
    }
}

5. 탐색 테스트 작성

시작 대상 확인

Cupcake 앱을 빌드할 때 앱의 탐색을 지시하는 상수가 포함된 CupcakeScreen라는 enum 클래스를 만들었습니다.

CupcakeScreen.kt

/**
* enum values that represent the screens in the app
*/
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)
}

UI가 있는 모든 앱에는 일종의 홈 화면이 있습니다. Cupcake의 경우 이 화면은 Start Order Screen입니다. CupcakeApp 컴포저블의 탐색 컨트롤러는 CupcakeScreen enum의 Start 항목을 사용하여 이 화면으로 이동할 시점을 결정합니다. 앱이 시작될 때 대상 경로가 아직 없으면 탐색 호스트 대상 경로는 CupcakeScreen.Start.name으로 설정됩니다.

앱이 시작될 때 Start Order Screen이 현재 대상 경로인지 확인하는 테스트를 먼저 작성해야 합니다.

  1. cupcakeNavHost_verifyStartDestination()이라는 함수를 만들고 @Test 주석을 추가합니다.
import org.junit.Test

@Test
fun cupcakeNavHost_verifyStartDestination() {
}

이제 탐색 컨트롤러의 초기 대상 경로가 Start Order Screen인지 확인해야 합니다.

  1. 예상 경로 이름(여기서는 CupcakeScreen.Start.name)이 탐색 컨트롤러의 현재 백 스택 항목의 대상 경로와 같음을 어설션합니다.
import org.junit.Assert.assertEquals
...

@Test
fun cupcakeNavHost_verifyStartDestination() {
    assertEquals(CupcakeScreen.Start.name, navController.currentBackStackEntry?.destination?.route)
}

도우미 메서드 만들기

UI 테스트에서는 종종 UI의 특정 부분을 테스트할 수 있는 상태에 UI를 배치하는 단계를 반복해야 합니다. 맞춤 UI에는 코드가 여러 줄 필요한 복잡한 어설션도 필요할 수 있습니다. 이전 섹션에서 작성한 어설션에는 코드가 많이 필요하며 Cupcake 앱에서 탐색을 테스트할 때 이와 동일한 어설션을 여러 번 사용합니다. 이러한 상황에서는 테스트에서 도우미 메서드를 작성하면 중복 코드를 작성하지 않아도 됩니다.

작성하는 각 탐색 테스트에서 CupcakeScreen enum 항목의 name 속성을 사용하여 탐색 컨트롤러의 현재 대상 경로가 올바른지 확인합니다. 이러한 어설션을 만들려고 할 때마다 호출할 수 있는 도우미 함수를 작성합니다.

이 도우미 함수를 만들려면 다음 단계를 완료하세요.

  1. test 디렉터리 ScreenAssertions에서 빈 Kotlin 파일을 만듭니다.

8c3c7146050db2a8.png

  1. NavController 클래스 assertCurrentRouteName()에 확장 함수를 추가하고 메서드 서명에서 예상되는 경로 이름의 문자열을 전달합니다.
fun NavController.assertCurrentRouteName(expectedRouteName: String) {

}
  1. 이 함수에서 expectedRouteName이 탐색 컨트롤러의 현재 백 스택 항목의 대상 경로와 같다고 어설션합니다.
import org.junit.Assert.assertEquals
...

fun NavController.assertCurrentRouteName(expectedRouteName: String) {
    assertEquals(expectedRouteName, currentBackStackEntry?.destination?.route)
}
  1. CupcakeScreenNavigationTest 파일을 열고 긴 어설션 대신 새 확장 함수를 사용하도록 cupcakeNavHost_verifyStartDestination() 함수를 수정합니다.
@Test
fun cupcakeNavHost_verifyStartDestination() {
    navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

많은 테스트에서는 UI 구성요소와 상호작용도 해야 합니다. 이 Codelab에서 이러한 구성요소는 리소스 문자열을 사용하여 찾는 경우가 많습니다. Context.getString() 메서드를 사용하여 리소스 문자열로 컴포저블에 액세스할 수 있습니다. 자세한 내용은 여기를 참고하세요. Compose에서 UI 테스트를 작성할 때 이 메서드를 구현하는 방법은 다음과 같습니다.

composeTestRule.onNodeWithText(composeTestRule.activity.getString(R.string.my_string)

이는 상세 명령이며 확장 함수를 추가하여 간소화할 수 있습니다.

  1. com.example.cupcake.test 패키지에 ComposeRuleExtensions.kt라는 새 파일을 만듭니다. 이 파일은 일반 Kotlin 파일이어야 합니다.

82762f66f890cf1b.png

  1. 이 파일에 다음 코드를 추가합니다.
import androidx.activity.ComponentActivity
import androidx.annotation.StringRes
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.rules.ActivityScenarioRule

fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.onNodeWithStringId(
    @StringRes id: Int
): SemanticsNodeInteraction = onNodeWithText(activity.getString(id))

이 확장 함수를 사용하면 문자열 리소스로 UI 구성요소를 찾을 때 작성하는 코드의 양을 줄일 수 있습니다. 다음과 같이 작성하는 대신

composeTestRule.onNodeWithText(composeTestRule.activity.getString(R.string.my_string)

이제 다음 명령을 사용할 수 있습니다.

composeTestRule.onNodeWithStringId(R.string.my_string)

시작 화면에 위로 버튼이 없는지 확인

Cupcake 앱의 원래 디자인에는 시작 화면의 툴바에 위로 버튼이 없습니다.

886fb7e824608c92.png

시작 화면에는 버튼이 없습니다. 초기 화면이므로 이 화면에서 위로 이동할 곳이 없기 때문입니다. 다음 단계에 따라 시작 화면에 위로 버튼이 없음을 확인하는 함수를 만듭니다.

  1. cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen()이라는 메서드를 만들고 @Test 주석을 추가합니다.
@Test
fun cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen() {
}

Cupcake의 Up 버튼에는 콘텐츠 설명이 R.string.back_button 리소스의 문자열로 설정됩니다.

  1. R.string.back_button 리소스의 값을 사용하여 테스트 함수에서 변수를 만듭니다.
@Test
fun cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen() {
    val backText = composeTestRule.activity.getString(R.string.back_button)
}
  1. 이 콘텐츠 설명이 있는 노드가 화면에 없다고 어설션합니다.
@Test
fun cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen() {
    val backText = composeTestRule.activity.getString(R.string.back_button)
    composeTestRule.onNodeWithContentDescription(backText).assertDoesNotExist()
}

맛 화면으로 이동 확인

시작 화면에서 버튼 중 하나를 클릭하면 탐색 컨트롤러에 맛 화면으로 이동하도록 지시하는 메서드가 트리거됩니다.

82c62c13891d627e.png

이 테스트에서는 버튼을 클릭하여 이 탐색을 트리거하고 대상 경로가 맛 화면인지 확인하는 명령어를 작성합니다.

  1. cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen()이라는 함수를 만들고 @Test 주석을 추가합니다.
@Test
fun cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen(){
}
  1. 문자열 리소스 ID로 One Cupcake 버튼을 찾아 클릭 작업을 실행합니다.
import com.example.cupcake.R
...

@Test
fun cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen() {
    composeTestRule.onNodeWithStringId(R.string.one_cupcake)
        .performClick()
}
  1. 현재 경로 이름이 맛 화면 이름이라고 어설션합니다.
@Test
fun cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen() {
    composeTestRule.onNodeWithStringId(R.string.one_cupcake)
        .performClick()
    navController.assertCurrentRouteName(CupcakeScreen.Flavor.name)
}

추가 도우미 메서드 작성

Cupcake 앱에는 대부분 선형 탐색 흐름이 있습니다. Cancel 버튼을 클릭하지 않으면 한 방향으로만 앱을 탐색할 수 있습니다. 따라서 앱 내부에서 더 깊은 화면을 테스트할 때는 테스트하려는 영역으로 이동하려고 코드를 반복해야 할 수 있습니다. 이러한 상황에서는 해당 코드를 한 번만 작성하면 되도록 더 많은 도우미 메서드를 사용하는 것이 좋습니다.

이제 맛 화면으로의 이동을 테스트했으므로 이후 테스트에서 해당 코드를 반복하지 않아도 되도록 맛 화면으로 이동하는 메서드를 만듭니다.

  1. navigateToFlavorScreen() 메서드를 만듭니다.
private fun navigateToFlavorScreen() {
}
  1. 이전 섹션에서와 마찬가지로 One Cupcake 버튼을 찾아 클릭 작업을 실행하는 명령어를 작성합니다.
private fun navigateToFlavorScreen() {
    composeTestRule.onNodeWithStringId(R.string.one_cupcake)
        .performClick()
}

맛 화면의 Next 버튼은 맛을 선택해야 클릭할 수 있습니다. 이 메서드는 탐색용 UI를 준비하기 위한 용도로만 사용됩니다. 이 메서드를 호출한 후에는 UI가 Next 버튼을 클릭할 수 있는 상태에 있어야 합니다.

  1. UI에서 R.string.chocolate 문자열이 있는 노드를 찾아 클릭 작업을 실행하여 노드를 선택합니다.
private fun navigateToFlavorScreen() {
    composeTestRule.onNodeWithStringId(R.string.one_cupcake)
        .performClick()
    composeTestRule.onNodeWithStringId(R.string.chocolate)
        .performClick()
}

수령 화면 및 요약 화면으로 이동하는 도우미 메서드를 작성할 수 있는지 확인합니다. 솔루션을 살펴보기 전에 이 연습을 직접 해 보세요.

다음 코드를 사용하면 됩니다.

private fun getFormattedDate(): String {
    val calendar = Calendar.getInstance()
    calendar.add(java.util.Calendar.DATE, 1)
    val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
    return formatter.format(calendar.time)
}
private fun navigateToPickupScreen() {
    navigateToFlavorScreen()
    composeTestRule.onNodeWithStringId(R.string.next)
        .performClick()
}

private fun navigateToSummaryScreen() {
    navigateToPickupScreen()
    composeTestRule.onNodeWithText(getFormattedDate())
        .performClick()
    composeTestRule.onNodeWithStringId(R.string.next)
        .performClick()
}

시작 화면 이외의 화면을 테스트할 때는 위로 버튼 기능을 테스트하여 이전 화면으로 이동하는지 확인해야 합니다. 위로 버튼을 찾아 클릭하는 도우미 함수를 만들어 보세요.

private fun performNavigateUp() {
    val backText = composeTestRule.activity.getString(R.string.back_button)
    composeTestRule.onNodeWithContentDescription(backText).performClick()
}

테스트 적용 범위 극대화

앱의 테스트 모음은 가능한 한 많은 앱 기능을 테스트해야 합니다. 완벽한 환경에서는 UI 테스트 모음이 UI 기능의 100%에 적용될 것입니다. 실제로는 이런 정도의 테스트 적용 범위를 달성하기 어렵습니다. 화면 크기가 고유한 기기, 다양한 버전의 Android 운영체제, 휴대전화의 다른 앱에 영향을 미칠 수 있는 서드 파티 앱 등 UI에 영향을 줄 수 있는 앱 외부의 요인이 많이 있기 때문입니다.

테스트 적용 범위를 극대화하는 데 도움이 되는 한 가지 방법은 기능을 추가할 때 기능과 함께 테스트를 작성하는 것입니다. 이렇게 하면 새로운 기능에 너무 앞서 나가거나 가능한 모든 시나리오를 기억하기 위해 이전으로 돌아가지 않아도 됩니다. 현재 Cupcake 앱은 매우 작은 앱이며 이미 앱 탐색의 상당 부분을 테스트했습니다. 그러나 테스트할 탐색 상태가 더 많이 있습니다.

다음 탐색 상태를 확인하는 테스트를 작성할 수 있는지 확인합니다. 솔루션을 살펴보기 전에 직접 구현해 보세요.

  • 맛 화면에서 위로 버튼을 클릭하여 시작 화면으로 이동합니다.
  • 맛 화면에서 취소 버튼을 클릭하여 시작 화면으로 이동합니다.
  • 수령 화면으로 이동합니다.
  • 수령 화면에서 위로 버튼을 클릭하여 맛 화면으로 이동합니다.
  • 수령 화면에서 취소 버튼을 클릭하여 시작 화면으로 이동합니다.
  • 요약 화면으로 이동합니다.
  • 요약 화면에서 취소 버튼을 클릭하여 시작 화면으로 이동합니다.
@Test
fun cupcakeNavHost_clickNextOnFlavorScreen_navigatesToPickupScreen() {
    navigateToFlavorScreen()
    composeTestRule.onNodeWithStringId(R.string.next)
        .performClick()
    navController.assertCurrentRouteName(CupcakeScreen.Pickup.name)
}

@Test
fun cupcakeNavHost_clickBackOnFlavorScreen_navigatesToStartOrderScreen() {
    navigateToFlavorScreen()
    performNavigateUp()
    navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

@Test
fun cupcakeNavHost_clickCancelOnFlavorScreen_navigatesToStartOrderScreen() {
    navigateToFlavorScreen()
    composeTestRule.onNodeWithStringId(R.string.cancel)
        .performClick()
    navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

@Test
fun cupcakeNavHost_clickNextOnPickupScreen_navigatesToSummaryScreen() {
    navigateToPickupScreen()
    composeTestRule.onNodeWithText(getFormattedDate())
        .performClick()
    composeTestRule.onNodeWithStringId(R.string.next)
        .performClick()
    navController.assertCurrentRouteName(CupcakeScreen.Summary.name)
}

@Test
fun cupcakeNavHost_clickBackOnPickupScreen_navigatesToFlavorScreen() {
    navigateToPickupScreen()
    performNavigateUp()
    navController.assertCurrentRouteName(CupcakeScreen.Flavor.name)
}

@Test
fun cupcakeNavHost_clickCancelOnPickupScreen_navigatesToStartOrderScreen() {
    navigateToPickupScreen()
    composeTestRule.onNodeWithStringId(R.string.cancel)
        .performClick()
    navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

@Test
fun cupcakeNavHost_clickCancelOnSummaryScreen_navigatesToStartOrderScreen() {
    navigateToSummaryScreen()
    composeTestRule.onNodeWithStringId(R.string.cancel)
        .performClick()
    navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

6. 주문 화면 테스트 작성

탐색은 Cupcake 앱 기능의 한 가지 측면일 뿐입니다. 또한 사용자는 각 앱 화면과 상호작용합니다. 이러한 화면에 표시되는 내용과 이러한 화면에서 실행된 작업이 올바른 결과를 생성하는지 확인해야 합니다. SelectOptionScreen은 앱에서 중요한 부분입니다.

이 섹션에서는 이 화면의 콘텐츠가 올바르게 설정되었는지 확인하는 테스트를 작성합니다.

맛 선택 화면 콘텐츠 테스트

  1. app/src/androidTest 디렉터리 내에 새 클래스 CupcakeOrderScreenTest를 만듭니다. 여기에는 다른 테스트 파일이 포함됩니다.

4409b8333eff0ba2.png

  1. 이 클래스에서 AndroidComposeTestRule을 만듭니다.
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
  1. selectOptionScreen_verifyContent()이라는 함수를 만들고 @Test 주석을 추가합니다.
@Test
fun selectOptionScreen_verifyContent() {

}

이 함수에서 최종적으로 Compose 규칙 콘텐츠를 SelectOptionScreen으로 설정합니다. 이렇게 하면 탐색이 필요하지 않도록 SelectOptionScreen 컴포저블이 직접 실행됩니다. 그러나 이 화면에는 두 가지 매개변수, 즉 맛 옵션 목록과 소계가 필요합니다.

  1. 화면에 전달할 맛 옵션 목록과 소계를 만듭니다.
@Test
fun selectOptionScreen_verifyContent() {
    // Given list of options
    val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
    // And subtotal
    val subtotal = "$100"
}
  1. 방금 만든 값을 사용하여 콘텐츠를 SelectOptionScreen 컴포저블로 설정합니다.

이 접근 방식은 MainActivity에서 컴포저블을 실행하는 것과 비슷합니다. 유일한 차이점은 MainActivityCupcakeApp 컴포저블을 호출하는 반면 여기서는 SelectOptionScreen 컴포저블이 호출된다는 것입니다. setContent()에서 실행되는 컴포저블을 변경할 수 있으면 테스트에서 명시적으로 앱을 단계별로 실행하여 테스트할 영역으로 이동하는 대신 특정 컴포저블을 실행할 수 있습니다. 이 접근 방식을 사용하면 현재 테스트와 관련 없는 코드 영역에서 테스트가 실패하는 것을 방지할 수 있습니다.

@Test
fun selectOptionScreen_verifyContent() {
    // Given list of options
    val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
    // And subtotal
    val subtotal = "$100"

    // When SelectOptionScreen is loaded
    composeTestRule.setContent {
        SelectOptionScreen(subtotal = subtotal, options = flavors)
    }
}

테스트의 이 지점에서 앱이 SelectOptionScreen 컴포저블을 실행하면 테스트 안내를 통해 이 컴포저블과 상호작용할 수 있습니다.

  1. flavors 목록을 반복하고 목록의 각 문자열 항목이 화면에 표시되는지 확인합니다.
  2. onNodeWithText() 메서드를 사용하여 화면에서 텍스트를 찾고 assertIsDisplayed() 메서드를 사용하여 텍스트가 앱에 표시되는지 확인합니다.
@Test
fun selectOptionScreen_verifyContent() {
    // Given list of options
    val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
    // And subtotal
    val subtotal = "$100"

    // When SelectOptionScreen is loaded
    composeTestRule.setContent {
        SelectOptionScreen(subtotal = subtotal, options = flavors)
    }

    // Then all the options are displayed on the screen.
    flavors.forEach { flavor ->
        composeTestRule.onNodeWithText(flavor).assertIsDisplayed()
    }
}
  1. 동일한 기법을 사용하여 앱이 텍스트를 표시하는지 확인하고 앱이 화면에 올바른 소계 문자열을 표시하는지 확인합니다. 화면에서 R.string.subtotal_price 리소스 ID와 올바른 소계 값을 검색한 후 앱이 값을 표시한다고 어설션합니다.
import com.example.cupcake.R
...

@Test
fun selectOptionScreen_verifyContent() {
    // Given list of options
    val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
    // And subtotal
    val subtotal = "$100"

    // When SelectOptionScreen is loaded
    composeTestRule.setContent {
        SelectOptionScreen(subtotal = subtotal, options = flavors)
    }

    // Then all the options are displayed on the screen.
    flavors.forEach { flavor ->
        composeTestRule.onNodeWithText(flavor).assertIsDisplayed()
    }

    // And then the subtotal is displayed correctly.
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(
            R.string.subtotal_price,
            subtotal
        )
    ).assertIsDisplayed()
}

Next 버튼은 항목을 선택해야 사용 설정됩니다. 이 테스트는 화면 콘텐츠만 확인하므로 테스트할 마지막 항목은 Next 버튼이 사용 중지되어 있는지입니다.

  1. 문자열 리소스 ID로 노드를 찾는 동일한 방식을 사용하여 Next 버튼을 찾습니다. 그러나 앱이 노드를 표시하는지 확인하는 대신 assertIsNotEnabled() 메서드를 사용합니다.
@Test
fun selectOptionScreen_verifyContent() {
    // Given list of options
    val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
    // And subtotal
    val subtotal = "$100"

    // When SelectOptionScreen is loaded
    composeTestRule.setContent {
        SelectOptionScreen(subtotal = subtotal, options = flavors)
    }

    // Then all the options are displayed on the screen.
    flavors.forEach { flavor ->
        composeTestRule.onNodeWithText(flavor).assertIsDisplayed()
    }

    // And then the subtotal is displayed correctly.
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(
            R.string.subtotal_price,
            subtotal
        )
    ).assertIsDisplayed()

    // And then the next button is disabled
    composeTestRule.onNodeWithStringId(R.string.next).assertIsNotEnabled()
}

테스트 적용 범위 극대화

맛 선택 화면 콘텐츠 테스트는 단일 화면의 한 측면만 테스트합니다. 코드 적용 범위를 넓히기 위해 작성할 수 있는 여러 가지 추가 테스트가 있습니다. 솔루션 코드를 다운로드하기 전에 다음 테스트를 직접 작성해 보세요.

  • 시작 화면 콘텐츠를 확인합니다.
  • 요약 화면 콘텐츠를 확인합니다.
  • 맛 선택 화면에서 옵션을 선택하면 Next 버튼이 사용 설정되는지 확인합니다.

테스트를 작성할 때는 작성하는 코드의 양을 줄일 수 있는 도우미 함수를 항상 염두에 두세요.

7. 솔루션 코드 가져오기

완료된 Codelab의 코드를 다운로드하려면 이 git 명령어를 사용하면 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git

또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

솔루션 코드를 보려면 GitHub에서 확인하세요.

8. 요약

축하합니다. Jetpack Navigation 구성요소를 테스트하는 방법을 알아봤습니다. 또한 재사용 가능한 도우미 메서드 작성, setContent()를 활용하여 간결한 테스트를 작성하는 방법, @Before 주석으로 테스트를 설정하는 방법, 최대 테스트 적용 범위를 고려하는 방법 등 UI 테스트를 작성하는 기본적인 기술을 알아봤습니다. Android 앱을 계속 빌드하면서 기능 코드와 함께 테스트를 계속 작성해야 합니다.