탐색 구성요소 테스트

1. 시작하기 전에

이전 Codelab에서는 탐색 구성요소를 사용한 탐색에 관해 알아봤습니다. 이 Codelab에서는 탐색 구성요소를 테스트하는 방법을 알아봅니다. 이는 탐색 구성요소를 사용하지 않고 탐색을 테스트하는 것과는 다릅니다.

기본 요건

  • Android 스튜디오에서 테스트 디렉터리를 만들어 보았습니다.
  • Android 스튜디오에서 단위 테스트와 계측 테스트를 작성해 보았습니다.
  • Android 프로젝트에 Gradle 종속 항목을 추가해 보았습니다.

학습할 내용

  • 계측 테스트를 사용하여 탐색 구성요소를 테스트하는 방법
  • 코드 반복 없이 테스트를 설정하는 방법

필요한 항목

  • Android 스튜디오가 설치된 컴퓨터
  • Words 앱의 솔루션 코드

이 Codelab의 시작 코드 다운로드

이 Codelab에서는 Words 앱의 솔루션 코드에 계측 테스트를 추가합니다.

  1. 프로젝트에 제공된 GitHub 저장소 페이지로 이동합니다.
  2. 브랜치 이름이 Codelab에 지정된 브랜치 이름과 일치하는지 확인합니다. 예를 들어 다음 스크린샷에서 브랜치 이름은 main입니다.

1e4c0d2c081a8fd2.png

  1. 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 팝업을 엽니다.

1debcf330fd04c7b.png

  1. 팝업에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
  2. 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
  3. ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.

Android 스튜디오에서 프로젝트 열기

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Open을 클릭합니다.

d8e9dbdeafe9038a.png

참고: Android 스튜디오가 이미 열려 있는 경우 File > Open 메뉴 옵션을 대신 선택합니다.

8d1fda7396afe8e5.png

  1. 파일 브라우저에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
  2. 프로젝트 폴더를 더블클릭합니다.
  3. Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
  4. Run 버튼 8de56cba7583251f.png을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.

2. 시작 앱 개요

Words 앱은 목록을 표시하는 홈 화면으로 구성되며 각 목록 항목은 알파벳 문자입니다. 문자를 클릭하면 클릭한 문자로 시작하는 단어 목록을 보여주는 화면으로 이동합니다.

3. 테스트 디렉터리 만들기

필요한 경우 이전 Codelab에서 한 것처럼 Words 앱용 계측 테스트 디렉터리를 만듭니다. 이 작업을 이미 완료했다면 필요한 종속 항목 추가로 건너뛰어도 됩니다.

4. 계측 테스트 클래스 만들기

androidTest 폴더에 NavigationTests.kt라는 새 클래스를 만듭니다.

b023023a2ccc3813.png

5. 필요한 종속 항목 추가

탐색 구성요소를 테스트하려면 몇 가지의 특정 Gradle 종속 항목이 필요합니다. 매우 구체적인 방식으로 프래그먼트를 테스트할 수 있게 해주는 종속 항목도 포함합니다. 앱 모듈의 build.gradle 파일로 이동하여 다음 종속 항목을 추가합니다.

androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.4.0'
androidTestImplementation 'androidx.navigation:navigation-testing:2.5.2'

debugImplementation 'androidx.fragment:fragment-testing:1.5.3'

이제 프로젝트를 동기화합니다.

6. 탐색 구성요소 테스트 작성

탐색 구성요소 테스트는 일반 탐색 테스트와 다릅니다. 일반 탐색을 테스트할 때는 기기나 에뮬레이터에서 이동이 실행되도록 합니다. 탐색 구성요소를 테스트할 때는 기기나 에뮬레이터가 눈에 보이게 이동하도록 하지 않습니다. 대신, 기기나 에뮬레이터에 표시되는 항목을 실제로 변경하지 않고 탐색 컨트롤러가 강제로 이동하도록 합니다. 그런 다음 탐색 컨트롤러가 정확히 대상에 도착했는지 확인합니다.

  1. navigate_to_words_nav_component()라는 테스트 함수를 만듭니다.
  2. 테스트에서 탐색 구성요소로 작업하려면 몇 가지 설정이 필요합니다. navigate_to_words_nav_component() 메서드에서 탐색 컨트롤러의 테스트 인스턴스를 만듭니다.
val navController = TestNavHostController(
   ApplicationProvider.getApplicationContext()
)
  1. 탐색 구성요소는 프래그먼트를 사용하여 UI를 구동합니다. 테스트용으로 프래그먼트를 분리하는 데 사용되는 ActivityScenarioRule과 동등한 프래그먼트가 있으며, 이는 프래그먼트별 종속 항목이 필요한 이유입니다. 이는 대상까지의 탐색을 처리하는 데 추가 코드 없이 실행할 수 있으므로 대상에 도달하기 위해 많은 탐색이 필요한 프래그먼트를 테스트하는 데 매우 유용합니다.
val letterListScenario = launchFragmentInContainer<LetterListFragment>(themeResId =
R.style.Theme_Words)

여기서는 LetterListFragment를 실행하도록 지정합니다. UI 구성요소가 사용할 테마를 알 수 있도록 앱의 테마를 전달해야 하며 그러지 않으면 테스트가 다운될 수 있습니다.

  1. 마지막으로, 탐색 컨트롤러가 프래그먼트 실행에 사용하려는 탐색 그래프를 명시적으로 선언해야 합니다.
letterListScenario.onFragment { fragment ->

   navController.setGraph(R.navigation.nav_graph)

   Navigation.setViewNavController(fragment.requireView(), navController)
}
  1. 이제 탐색을 실행하는 이벤트를 트리거합니다.
onView(withId(R.id.recycler_view))
   .perform(RecyclerViewActions
       .actionOnItemAtPosition<RecyclerView.ViewHolder>(2, click()))

launchFragmentInContainer() 메서드를 사용할 경우 이동하고 있을 수 있는 다른 프래그먼트나 활동을 컨테이너가 인식하지 못하므로 실제 탐색이 불가능합니다. 이 컨테이너는 컨테이너에서 실행하도록 지정된 프래그먼트만 인식합니다. 따라서 기기 또는 에뮬레이터에서 이 테스트를 실행하면 실제 탐색이 표시되지 않습니다. 이 방법은 직관적이지 않은 것처럼 보일 수도 있지만, 이를 통해 현재 대상에 관한 훨씬 더 직접적인 어설션이 가능합니다. 특정 화면에 표시되는 것으로 알려진 UI 구성요소를 찾는 대신, 현재 탐색 컨트롤러의 대상이 예상한 프래그먼트의 ID인지 확인하면 됩니다. 이 방식은 앞서 언급한 방식보다 훨씬 신뢰할 만합니다.

assertEquals(navController.currentDestination?.id, R.id.wordListFragment)

테스트 결과는 다음과 같이 표시됩니다. 78b4a72f75134ded.png

7. 솔루션 코드

8. 주석으로 코드 반복 방지

Android의 경우 계측 테스트와 단위 테스트 모두 코드 반복 없이 클래스의 모든 테스트에 동일한 구성을 설정할 수 있는 기능이 있습니다.

예를 들어 버튼이 10개인 프래그먼트가 있다고 가정해 보겠습니다. 각 버튼을 클릭하면 고유한 프래그먼트로 연결됩니다.

위 테스트의 패턴을 따랐다면 10개의 테스트 각각에 다음과 같은 코드를 반복했어야 합니다(이 코드는 예제일 뿐이며 이 Codelab에서 사용한 앱에서는 컴파일되지 않음).

val navController = TestNavHostController(
    ApplicationProvider.getApplicationContext()
)

val exampleFragmentScenario = launchFragmentInContainer<ExampleFragment>(themeResId =
R.style.Theme_Example)

exampleFragmentScenario.onFragment { fragment ->

   navController.setGraph(R.navigation.example_nav_graph)

   Navigation.setViewNavController(fragment.requireView(), navController)
}

10회나 반복하기에는 꽤 많은 양의 코드입니다. 그러나 이 경우에는 JUnit에서 제공하는 @Before 주석을 사용하여 시간을 절약할 수 있습니다. 이를 사용하려면 메서드에 주석을 추가하고, 이 메서드에서 테스트를 설정하는 데 필요한 코드를 제공하면 됩니다. 메서드 이름은 원하는 대로 지정할 수 있지만 관련성이 있어야 합니다. 동일한 프래그먼트를 10회 설정하는 대신, 다음과 같이 한 번만 설정 코드를 작성하면 됩니다.

lateinit var navController: TestNavHostController

lateinit var exampleFragmentScenario: FragmentScenario<ExampleFragment>

@Before
fun setup(){
    navController = TestNavHostController(
        ApplicationProvider.getApplicationContext()
    )

    exampleFragmentScenario =  launchFragmentInContainer(themeResId=R.style.Theme_Example)

    exampleFragmentScenario.onFragment { fragment ->

       navController.setGraph(R.navigation.example_nav_graph)

       Navigation.setViewNavController(fragment.requireView(),  navController)
    }
}

이 메서드는 이제 이 클래스에 작성한 모든 테스트를 실행하며, 여러 테스트 중 하나의 테스트에서 필요한 변수에 액세스할 수 있습니다.

마찬가지로, 매 테스트 후 실행해야 하는 코드가 있다면 @After 주석을 사용하면 됩니다. 예를 들어, @After는 테스트에 사용한 리소스를 정리하는 데 사용할 수 있으며, 계측 테스트의 경우 기기를 특정 상태로 반환하는 데 사용할 수 있습니다.

JUnit은 @BeforeClass@AfterClass 주석도 제공합니다. 이 주석을 사용하는 메서드는 한 번 실행되지만, 실행된 코드는 계속 모든 메서드에 적용된다는 점이 앞의 주석과 다릅니다. 설정 또는 해제 메서드에 고비용 작업이 포함되어 있으면 이러한 주석을 사용하는 것이 더 좋을 수 있습니다. @BeforeClass@AfterClass 주석이 달린 메서드는 컴패니언 객체에 배치되고 @JvmStatic 주석이 붙어 있어야 합니다. 이러한 주석의 실행 순서를 알아보기 위해 다음 코드를 살펴보겠습니다.

5157ab00a9b7fb84.png

@BeforeClass는 클래스를 대상으로 실행되고 @Before는 함수보다 먼저 실행되며 @After는 함수 이후에 실행되고 @AfterClass는 클래스를 대상으로 실행됩니다. 어떻게 출력될지 예상할 수 있나요?

39c04aa2ba7b8348.png

함수의 실행 순서는 setupClass(), setupFunction(), test_a(), tearDownFunction(), setupFunction(), test_b(), tearDownFunction(), setupFunction(), test_c(), tearDownFunction(), tearDownClass()입니다. 이는 @Before@After가 각각 모든 메서드의 전과 후에 실행되기 때문에 그렇습니다. @BeforeClass는 클래스의 어떤 항목이 실행되기 전에 한 번 실행되며 @AfterClass는 클래스의 모든 항목이 실행된 후에 한 번 실행됩니다.

9. 축하합니다

이 Codelab에서는 다음 사항을 다루었습니다.

  • 탐색 구성요소를 테스트하는 방법
  • @Before, @BeforeClass, @After@AfterClass 주석을 사용하여 반복 코드를 방지하는 방법