RIEN😚
이상한 나라의 개발자
RIEN😚
전체 방문자
오늘
어제
  • 분류 전체보기 (125)
    • Algorithm (68)
      • 알고리즘 (0)
      • Baekjoon (8)
      • 프로그래머스 (55)
      • HackerRank (5)
    • Android (30)
      • Project (1)
      • Error (2)
      • Studio (1)
      • Android (26)
    • Kotlin (6)
    • CS (4)
      • 네트워크 (2)
      • 데이터베이스 (2)
    • Front End (5)
      • React (1)
      • VUE (3)
      • Project (0)
      • 기타 (1)
    • 기록 (11)
      • 회고록 (6)
      • TIL (5)

블로그 메뉴

  • Github🔥
  • 포트폴리오🌹

공지사항

인기 글

티스토리

250x250
반응형
hELLO · Designed By 정상우.
RIEN😚

이상한 나라의 개발자

Android/Android

[Android:Codelab] Dependency Injection and Test Doubles - 5

2023. 1. 26. 16:28
728x90
반응형
 

Advanced Android in Kotlin 05.2: Introduction to Test Doubles and Dependency Injection  |  Android Developers

In this codelab you’ll learn to set up manual dependency injection, a service locator, and how to use fakes and mocks in your Android Kotlin apps. In doing so, you’ll learn how to test a repository and write fragment integration tests.

developer.android.com

안드로이드 독학하기!

테스트 Codelab을 이어서 진행하고 있습니다.😆

 

10. Espresso

Integration test의 끝판왕이라고 할 수 있는 Espresso UI tesint 라이브러리에 대해서 학습해보겠습니다.

 

Espresso는

  • view와 상호작용할 수 있습니다. (ex. 버튼 클릭, 바 슬라이딩, 스크롤 등)
  • 특정 view가 화면에 보이는지 또는 특정 상태인지 확인할 수 있습니다. (ex. 특정 문자를 포함하는가, checkbox가 선택되어있는가 등)

 

10.1 Dependency 추가하기

기본적으로 프로젝트를 생성하면 core Espresso Dependency가 추가되어 있는 것을 알 수 있습니다.

 

10.2 기기의 animation 끄기

Espresso test는 실제 기기에서 이루어집니다. 실제와 같이 테스트해볼 수 있는 장점이 있지만 애니메이션이 문제가 될 수 있습니다.

애니메이션의 진행정도에 따라 테스트가 실패할 수도 있고 성공할 수도 있기 때문입니다.(Flaky하다)

 

때문에 Developer Option에서 애니메이션을 전부 꺼주는 것이 좋습니다.

  • Window animation scale
  • Transition animation scale
  • Animator duration scale

 

10.3 Espresso test 둘러보기

Espresso test를 작성하기 전에 간단히 Espresso test에 대해서 알아보겠습니다.

 

Ex. task_detail_complete_checkbox id를 가지는 checkbox view를 click했을 때,

선택되었는지 검사하는 테스트는 아래와 같이 작성할 수 있습니다.

onView(withId(R.id.task_detail_complete_checkbox))
	.perform(click())
    .check(matches(isChecked()))

Espresso는 주로 4가지 부분으로 이루어져 있습니다.

 

10.3.1 Static Espresso method
onView

- Espresso 문장은 static Espresso method로 시작합니다.

- onView는 가장 흔하게 사용되는 것 중에 하나로, onData라는 것도 있습니다.

 

👇🏻 그외

closeSoftKeyboard() 만약 키보드가 열려있다면 닫음
@CheckReturnValue
onData(Matcher<Object> dataMatcher)
app에 의해 보여지는 data 객체를 위한 DataInteraction 생성
onIdle() app이 idle 될 때까지 main thread를 돌림
<T> onIdle(Callable<T> action) app이 idle 될 때까지 main thread를 돌림
@CheckReturnValue
onView(Matcher<View> viewMatcher)
주어진 view의 ViewInteraction 생성
openActionBarOverflowOrOptionsMenu(Context context) ActionBar에 보이는 overflow menu를 엶
openContextualActionModeOverflowMenu() ActionModel의 contextual options에 보여지는 overflow menu를 엶
pressBack() back button 누름
pressBackUnconditionally() pressBack과 비슷하지만 Espress navigates가
앱 밖으로 나가도 exception을 발생시키지 않는다.
setFailureHandler(FailureHandler failureHandler) default FailureHandler를 주어진 handler로 교체한다.

 

10.3.2 ViewMatcher
withId(R.id.task_detail_title_text)

ViewMatchers가 너무 다양해서 표로 옮기는 것은 포기하겠습니다. 😣

 

위와 같이 View의 정보를 가져올 수도 있고, View가 화면에 보이느지 검사하거나 상태를 확인할 수 있습니다.

👉🏻 다른 ViewMatchers는 여기서!

 

10.3.3 ViewAction
perform(click())

- perform이 ViewAction 메서드입니다.

- 이름 그대로 view가 할 수 있는 동작을 수행할 수 있습니다.

🌹 동작이 수행될 필요가 있는 경우에만 perform 메서드를 사용하도록 합시다.

 

10.3.4 ViewAssertion
check(matches(isChecked())

- check은 ViewAssertion를 파라미터로 받아 검사하는 메서드입니다.

 

👇🏻 실제 테스트 코드는 아래와 같습니다.

 @Test
fun activeTaskDetails_DisplayedInUi() = runTest {
    // 1. Given: Add active (incomplete) task to the DB
    val activeTask = Task("ActiveTask", "AndroidX Rocks", false)
    repository.saveTask(activeTask)

    // 2. When: TaskDetailFragment launched to display task
    val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
    launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    // 3. Then: Task details are displayed on the screen
    onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
    onView(withId(R.id.task_detail_title_text)).check(matches(withText("ActiveTask")))
    onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
    onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
    // and make sure the "active" checkbox is shown unchecked
    onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
    onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}

 

모든 테스트가 성공적으로 완료되었습니다.👏🏻👏🏻👏🏻👏🏻

 

10. Mockito

이번에 진행한 코드랩의 마지막 Chapter입니다. 

Navigation component 테스트를 위해 Mockito를 공부해보도록 하겠습니다.🤗

 

지금까지는 Test Double 중 Fake만들 사용해 Data Source도 만들어보고 Repository도 만들어 보았는데요.

Navigation Component를 테스트하기 위해서는 어떠한 Test Double를 써서 어떻게 테스트 해야 할까요?

 

프로젝트에서 실제로 Navigation이 일어나는 곳을 확인해보겠습니다.

 

위 화면은 프로젝트의 메인 화면입니다. 여기서 Task 아이템을 클릭하면 상세 화면으로 이동하게 됩니다.

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}

 

navigation이 일어나기 위해서는 코드 상에서와 마찬가지로 navigate 메서드를 호출해주어야 하며,

지금까지와 마찬가지로 특정 상태 값만을 가지고 정상적으로 작동하였는지 검사하기에 Navigation 동작은 너무나도 복잡합니다.😭

 

이 경우에는 navigation 메서드가 알맞은 파라미터와 함꼐 호출되었는지 여부를 검사하는 방식으로 테스트할 수 있습니다.

그리고 이 때 사용할 수 있는 것이 바로 mock Test double 이죠.

(* 즉, 특정 메서드가 정확히 호출되었는지 여부를 확인하는 것이죠!🤗)

🌹 Mock Test Double
- 어떠한 동작을 하도록 프로그래밍 했을 때, 각 클래스 또는 메서드 사이에서 적절한 상호작용이 발생하도록 하는 테스트 더블입니다.
- Mocks는 개발자가 정의한 대로 상호작용이 이루어지지 않았을 때 실패합니다.
- 주로 mocking framework에 의해 만들어집니다.

Ex. 특정 메서드가 단 한번만 호출되었는지 검사하는 경우

 

Mockito가 바로 Mock Test Double를 만들어주는 프레임워크입니다.

이름에 Mock이라는 단어가 대놓고 있지만 Mock Test Double만을 만들지 않고 stubs나 spies를 만들기도 합니다.

 

이 곳에서는 navigate 메서드가 적절히 호출되었는지 확인할 수 있는 mock NavigationController를 만들기 위해 Mockito를 사용합니다.

 

10.1 Dependency 추가하기

// Dependencies for Android instrumented unit test
androidTestImplementation "org.mockito:mockito-core:4.8.0"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.28.1"
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.5.1"

 

10.1.1 dexmarker-mockito

- 이 라이브러리는 Android에서 Mockito를 사용하는데 필요한 dependency 입니다.

- Mockito는 런타임 시에 객체를 생성할 필요가 있는데, 안드로이드에서는 dex byte code를 사용하여 이 작업을 수행해줍니다.

 

👇🏻 dex bytecode가 뭐쥐?🤔

 

Diving deep into Android Dex bytecode

Analyzing memory and performance of our code at the low-level.

proandroiddev.com

 

10.1.2 androidx.test.espresso:espresso-contrib

- 이 라이브러리는 DatePicker나 RecyclerView와 같이 좀 더 복잡한 view를 위한 테스트 코드를 포함하고 있습니다.

- 또한 Accessibility를 검사할 때도 사용될 수 있습니다.

 

10.2 테스트 코드 작성하기

 

10.2.1 TasksFragmentTest 클래스 만들기

- 이번에는 TasksFragment를 가지고 테스트 코드를 만들어보도록 하겠습니다.

@MediumTest
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
internal class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanUpDb() = runTest {
        ServiceLocator.resetRepository()
    }
}

이전에 작성했던 TasksDetailFragmentTest와 거의 동일하기 때문에 어려운 점은 없습니다.

 

10.2.2 테스트 함수 만들기

Navigate를 테스트하기 전에 TasksFragment를 먼저 구성하도록 하겠습니다.

@Test
fun clickTask_navigateToDetailFragmentOne() = runTest {
    repository.saveTask(Task("TITLE1", "Description1", false, "id1"))
    repository.saveTask(Task("TITLE2", "Description2", true, "id2"))

    // 1. Given
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    Thread.sleep(5000)
}

 

10.2.3 Mock Test Double 만들기

이제 Mockito를 활용해봅시다🔥

먼저 NavController의 Mock Object를 만들기 위해 Mockito의 mock 메서드 호출해줍니다.

val navController = mock(NavController::class.java)
🌹 Mock Object는 OOP에서 실제 객체의 동작을 모방하는 simulated objects이며, 테스트에서 객체 초기화 시에 자주 사용됩니다. - 위키피디아

 

10.2.4 onFragment

추가적으로 NavController와 Fragment를 이어주는 작업이 필요합니다.

scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}

전달된 람다함수를 현재 Activity의 main thread에서 실행합니다.

// 실제로 내부 코드를 보면 아래와 같이 되어있다
activityScenario.onActivity { activity -> }

또한 현재 FragmentScenario가 가리키는 Fragment를 매개변수로 받아 사용할 수 있습니다.

때문에 위 onFragment에 전달된 람다함수 내의 it은 TasksFragment를 가리킵니다.

 

여기서 추가로 setViewNavController에 대해 알아보고 다음으로 넘어가도록 하겠습니다. 😊

📌 setViewNavController

 public static void setViewNavController(
 	@NonNull View view,
    @Nullable NavController controller
) {
    view.setTag(R.id.nav_controller_view_tag, controller);
}

위와 같이 파라미터로 전달된 view의 tag로 navController를 setting 해주는 코드입니다.

왜 View와 NavController를 연결시켜주는 작업이 필요한 것일까? 🤔

 

실제 TasksFragment에서 navigate 메서드를 호출하는 부분을 보면 그 이유를 알 수 있습니다!

findNavController().navigate(action)

이렇게 findNavController()를 호출해서 NavController를 찾는 것을 알 수 있습니다.

 

이 코드를 타고타고 계속 들어가다보면 아래와 같은 코드를 볼 수 있습니다.

@Nullable
private static NavController getViewNavController(@NonNull View view) {
    Object tag = view.getTag(R.id.nav_controller_view_tag);
    NavController controller = null;
    if (tag instanceof WeakReference) {
        controller = ((WeakReference<NavController>) tag).get();
    } else if (tag instanceof NavController) {
        controller = (NavController) tag;
    }
    return controller;
}

setViewNavController에서 추가해준 tag를 가져와 사용하고 있습니다🤗👍🏻

 

🤔 제 생각에는 Fragment에서 NavController를 사용 시, Fragment에 속한 NavController를 찾기 위해 Fragment의 View와 NavController를 연결시켜주는 것이 아닐까 합니다.( * 제 개인적인 생각입니다. )

 

10.2.5 RecyclerView의 아이템 중 "TITLE1"을 클릭하는 코드 추가
onView(withId(R.id.tasks_list))
    .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
        hasDescendant(withText("TITLE1")),
        click()
    ))

RecyclerViewActions는 espress-contrib 라이브러리에서 제공해주는 클래스입니다.

 

👇🏻 다른 RecyclerViewAction

scrollTo() 전달된 view가 존재한다면 해당 view로 이동
scrollToHolder() 전달된 viewHolder가 존재한다면 해당 viewHolder로 이동
scrollToPosition() 해당 position으로 이동
actionOnHolderItem() 전달된 ViewHolder와 일치하는 아이템에서 Action 수행
actionOnItem() 전달된 View와 일치하는 아이템에서 Action 수행
actionOnItemAtPosition() 해당 position 아이템에서 Action 수행
🍿 hasDescendant: view 계층의 하위에서 주어진 조건에 맞는 view를 찾아 matcher로 반환

 

10.2.6 navigate가 정확한 argument로 호출되었는지 확인
// 3. Then
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment("id1")
)

- verify는 파라미터로 전달한 Mock 객체와 동일한 타입의 객체를 반환해주기 때문에 navigate 메서드를 호출할 수 있다.

- 위의 코드는 navController Mock 객체의 navigate 메서드가 주어진 파라미터로 호출되었는지 확인하는 테스트 코드입니다.

 

위의 모든 테스트 코드를 작성하고 실행해보면

성공적으로 테스트가 완료된 것을 알 수 있습니다.

 

10.3 요약해보면.

  1. Mockito를 사용해 NavController Mock 객체를 만들어줍니다.
  2. NavController Mock 객체를 Fragment View와 연결해줍니다.
  3. navigate 메서드가 적절히 호출되었는지 verify 메서드를 통해 확인해줍니다.

 


😭😭 길고 길었던 Test 코드랩 그 두 번째가 완료되었습니다.

뭔가 새로 알게 된 점이 많아 다시 복습이 필요할 듯 하네요.ㅎㅎ

 

다음은 마지막 Test 코드랩인 Testing Coroutines and Jetpack integrations 입니다.

 

감사합니다. 😌

반응형

'Android > Android' 카테고리의 다른 글

[공식문서 열어보기] AlarmManger로 정확한 시간에 알림 띄워주자  (0) 2023.02.13
[Hilt] Component와 Scope 무엇이 다른지 디버깅으로 다 찍어보자!  (0) 2023.02.11
[Android:Codelab] Dependency Injection and Test Doubles - 4  (0) 2023.01.25
[Android:Codelab] Dependency Injection and Test Doubles - 3  (0) 2023.01.25
[Android:Codelab] Dependency Injection and Test Doubles - 2  (0) 2023.01.24
    'Android/Android' 카테고리의 다른 글
    • [공식문서 열어보기] AlarmManger로 정확한 시간에 알림 띄워주자
    • [Hilt] Component와 Scope 무엇이 다른지 디버깅으로 다 찍어보자!
    • [Android:Codelab] Dependency Injection and Test Doubles - 4
    • [Android:Codelab] Dependency Injection and Test Doubles - 3
    RIEN😚
    RIEN😚
    안드로이드 / 코틀린 독학으로 취업하자!

    티스토리툴바