IT/Android (안드로이드)

Android N.4-4 Testing: Intro to Test Doubles & Dependency Injection

Edmond Na 2025. 4. 21. 23:32

2021.4.14 작성

참고 - https://black-jin0427.tistory.com/111

#0 UI 테스트

단일 앱 내에서 사용자 상호작용을 테스트하면 사용자가 앱과 상호작용할 때 예상치 못한 결과가 발생하거나 불만족스러운 경험을 하지 않도록 할 수 있습니다. 앱의 사용자 인터페이스(UI)가 올바르게 작동하는지 확인해야 한다면 UI 테스트를 만드는 습관을 들여야 합니다.

#1 Espresso를 이용한 UI 테스트 방법

  1. 먼저 build.gradle에 필요한 라이브러리를 넣습니다.
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
  1. 에뮬레이터의 애니메이션을 끕니다.

Espresso UI 테스트의 경우 애니메이션을 끄는 것이 가장 좋습니다

  1. 테스트 기기에서 설정> 개발자 옵션으로 이동합니다 .
  2. 창 애니메이션 배율 , 전환 애니메이션 배율Animator 기간 배율의 세 가지 설정을 비활성화합니다 .
  1. src/androidText/java 에 HelloWorldEspressoTest 파일을 생성합니다.
@RunWith(AndroidJUnit4.class)  
class HelloWorldEspressoTest {  

    @Rule  
    val  mActivityRule = ActivityTestRule(MainActivity.class)  

    @Test  
    fun listGoesOverTheFold() {  

        //editText 에 Hello World! 입력하고 키보드를 내립니다.  
        Espresso.onView(withId(R.id.editText)).perform(typeText("Hello World!"), closeSoftKeyboard());  

        //textView 의 값이 "Hello World!" 인지 확인합니다.  
        Espresso.onView(withId(R.id.textView)).check(matches(withText("Hello World!")));  

        //button 을 클릭합니다.  
        Espresso.onView(withId(R.id.button)).perform(click());  
    }  
}

#2 Mockito란?

Mockito는 객체를 Mocking하는데 사용되는 Java 기반의 라이브러리입니다. JUnit과 함께 Unit Test를 작성하는데 사용됩니다. Android도 Unit Test를 작성하는데 공식적으로 Mockito를 사용하도록 권장하고 있습니다.

(mocking : 쉽게 말해서 흉내낸다는 것을 의미한다.)

여기서 Mockito를 이용해서 Android Jetpack Navigation을 테스트 해보려 합니다.

#3 Mockito를 이용한 테스트 방법 (Navigation)

  1. build.gradle에 코드를 추가해줍니다.
    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
  1. TaskFragmentTest 파일을 생성해줍니다.

먼저 해당 파일인 TaskFragment에 들어가서 클래스 이름을 우클릭 후, Generate -> Test를 클릭해서 TaskFragmentTest파일을 생성해줍니다.

이때 ServiceLocator는 object로 선언한 LocalDatabase입니다.

먼저 ServiceLocator 예제입니다.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

그런 다음 테스트를 진행할 Repository와 LocalDatabase를 Set합니다.

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

    private lateinit var repository: TasksRepository

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

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}
  1. clickTask_navigateToDetailFragmentOne 테스트 메서드를 만든 뒤 테스트를 진행합니다.
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)

    val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}