[Android TDD CodeLab 02.Introduction to Test Doubles and Dependency Injection] part3

나고수·2022년 10월 23일
0

codelab

목록 보기
4/5

Android TDD CodeLab 02.Introduction to Test Doubles and Dependency Injection Tasks. 10~15

Task: Writing your first Integration Test with Espresso

목표 : Espresso UI 테스트 라이브러리를 사용하여 통합 테스트하기 . TaskDetailFragment의 뷰를 눌렀을 때 원하는대로 작동이 되는지 테스트

Espresso

Interact with views, like clicking buttons, sliding a bar, or scrolling down a screen.
Assert that certain views are on screen or are in a certain state (such as containing particular text, or that a checkbox is checked, etc.).

Step 1. Note Gradle Dependency

androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"

This core Espresso dependency is included by default when you make a new Android project. It contains the basic testing code for most views and actions on them.

Step 2. Turn off animations

에스프레소 테스트를 flacky 하지 않게 만들기 위하여 개발자 설정에서
Window animation scale, Transition animation scale, and Animator duration scale.를 비활성화 시킵니다.

Step 3. Look at an Espresso test

//id가 task_detail_complete_checkbox인 뷰를 클릭 한 후 , 체크되었는지 확인하는 코드 
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
  • 에스프레소는 보통 다음과 같은 4가지 파트로 구성 되어 있다.
  1. Static Espresso method - onView
    onView는 Espresso 문을 시작하는 정적 Espresso 메서드의 예입니다. onView는 가장 일반적인 옵션 중 하나이지만 onData와 같은 다른 옵션도 있습니다.
  2. viewMatcher - withId(R.id.task_detail_title_text)
    withId는 해당 ID로 view를 가져오는 ViewMatcher의 예입니다. 다른 matcher 보러가기
  3. viewAction - perform(click())
    something that can be done to the view
  4. ViewAssertion - check(matches(isChecked()))
    ViewAssertions check or assert something about the view.
    @Test
    fun activeTaskDetails_DisplayedInUi() =
        runTest {
            // GIVEN - Add active (incomplete) task to the DB
            val activeTask = Task("Active Task", "AndroidX Rocks", false)
            repository.saveTask(activeTask)

            // WHEN - Details fragment launched to display task
            val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
            //FragmentScenario 생성
            launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
            // THEN - Task details are displayed on the screen
            // make sure that the title/description are both shown and correct
            onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
            onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
            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())))
        }

Step 4. Optional, Write your own Espresso Test

package com.example.android.architecture.blueprints.todoapp.taskdetail

import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.FakeAndroidTestRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest
import org.hamcrest.core.IsNot.not
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

//실행할 테스트 크기를 그룹화하고 선택하는 데 도움이 됩니다.
//SmallTest - 단위테스트
//MediumTest - 통합테스트
//LargeTest - E2E테스트
@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

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

//    @Test
//    fun activeTaskDetails_DisplayedInUi() =
//        runTest {
//            // GIVEN - Add active (incomplete) task to the DB
//            val activeTask = Task("Active Task", "AndroidX Rocks", false)
//            repository.saveTask(activeTask)
//
//            // WHEN - Details fragment launched to display task
//            val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
//            //FragmentScenario 생성
//            launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
//            //TaskDetailFragment를 테스트 하려면 , TaskDetailViewModel에서 만드는 repository를 fakeRepository로 바꿔줘야한다.
//            //fragment를 구성하지 않기 때문에-직접 만들지 않기 때문에? 아니면 fragment에 파라미터를 넣는건 원칙적으로 불허용이라?-, TaskDetailViewModel 종속정 주입이 어렵다.
//            //서비스 로케이터(Service Locator) 사용
//            //싱글 톤 클래스를 만드는 것을 포함하며, 그 목적은 일반 코드와 테스트 코드 모두에 종속성을 제공하는 것이다.
//        }

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

            // WHEN - Details fragment launched to display task
            val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
            //FragmentScenario 생성
            launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
            // THEN - Task details are displayed on the screen
            // make sure that the title/description are both shown and correct
            onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
            onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
            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())))
        }

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest {
        // GIVEN - Add completed task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", true)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }


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

Task: Using Mockito to write Navigation tests

목표 : mock이라고 하는 다른 유형의 test double과 테스트 라이브러리인 Mockito를 사용하여 Navigation 구성 요소를 테스트하는 방법을 배웁니다.

상황 : TaskFragment.kt에 있는 작업 하나를 눌러서 detailFragment로 이동하기

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

mock은 특정 메서드가 호출되었는지 여부를 확인합니다.
Mockito는 testDouble을 만들기 위한 프레임워크입니다. mock라는 단어는 API와 이름에 사용되지만 단지 mock를 만들기 위한 것은 아닙니다. stub과 spy도 만들 수 있습니다.
Mockito를 사용하여 탐색 메서드가 올바르게 호출되었음을 확인할 수 있는 모의 NavigationController를 만들 것입니다.

Step 1. Add Gradle Dependencies

//This is the Mockito dependency.
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
//이 라이브러리는 Android 프로젝트에서 Mockito를 사용하는 데 필요합니다. 
//Mockito는 런타임에 클래스를 생성해야 합니다. 
//Android에서 이것은 dex 바이트 코드를 사용하여 수행되므로 이 라이브러리를 사용하면 Android에서 런타임 중에 Mockito가 객체를 생성할 수 있습니다.
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 
//이 라이브러리는 DatePicker 및 RecyclerView와 같은 고급 view에 대한 테스트 코드가 포함되어 있습니다.
//또한 Accessibility 검사 및 나중에 다룰 CountingIdlingResource라는 클래스도 포함합니다.
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"

Step 2. Create TasksFragmentTest

Step 3. Optional, write clickAddTaskButton_navigateToAddEditFragment

TasksFragment.kt blocking 후 androidTest에 testclass 만들기

package com.example.android.architecture.blueprints.todoapp.tasks

import android.content.Context
import android.os.Bundle
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.FakeAndroidTestRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify

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

//탐색은 탐색 메소드에 대한 호출로 인해 발생합니다. assert 문을 작성해야 하는 경우 TaskDetailFragment로 이동했는지 여부를 테스트하는 간단한 방법은 없습니다.
//단언할 수 있는 것은 탐색 메소드가 올바른 조치 매개변수로 호출되었다는 것입니다.
//이것이 바로 mock test double이 하는 일입니다. 특정 메서드가 호출되었는지 여부를 확인합니다.
//Mockito는 테스트 더블을 만들기 위한 프레임워크입니다. mock이라는 단어는 API와 이름에 사용되지만 단지 mock를 만들기 위한 것은 아닙니다. stub과 spy도 만들 수 있습니다.

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

    private lateinit var repository: TasksRepository

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

    @After
    fun cleanUp() = runBlocking {
        ServiceLocator.resetRepository()
    }

    //특정 Task를 눌렀을때, 그 task 의 deatilFragment로 넘어가는지 테스트
    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlocking {
        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
        ) //test용 fragment
        val navController =
            mock(NavController::class.java) //mock(테스트용으로 만들고 싶은 class) //test용 navController
        //scenario와 navController 연결시키기
        scenario.onFragment {
            //setViewNavController
            //Associates a NavController with the given View,
            // allowing developers to use findNavController(View) and findNavController(Activity, int)
            // with that View or any of its children to retrieve the NavController.
            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")
        )

    }

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() = runBlocking {
        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 "+" button
        onView(withId(R.id.add_task_fab)).perform(click())
        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

}
profile
되고싶다

0개의 댓글