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가 화면에 보이느지 검사하거나 상태를 확인할 수 있습니다.
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 요약해보면.
- Mockito를 사용해 NavController Mock 객체를 만들어줍니다.
- NavController Mock 객체를 Fragment View와 연결해줍니다.
- 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 |