Development record of developer who study hard everyday.

레이블이 안드로이드JUnit인 게시물을 표시합니다. 모든 게시물 표시
레이블이 안드로이드JUnit인 게시물을 표시합니다. 모든 게시물 표시
, , , ,

JUnit과 Mockito로 안드로이드 테스트 코드 작성하기

 JUnit과 Mockito로 안드로이드 테스트 코드 작성하기

안드로이드 블로그

안드로이드 테스트 코드 작성의 이점

- 잘못된 부분을 빠르게 확인할 수 있다.

- 개발 시간이 빨라진다.

- 좋은 구조로 개발하게 된다.

안드로이드 테스트 종류

Unit Test

  • {module_name}/src/test/java 경로에 테스트 코드 작성
  • 빠르다
  • JVM에서 실행되는 테스트
  • 안드로이드 프레임워크와 종속성이 없거나 모의 객체를 생성할 수 있는 경우 사용
  • JUnit, Mockito, PowerMock, Truth, Robolectric

Unit Test 설명 그림

Instrumentation Test

  • {module_name}/src/androidTest/java 경로에 테스트 코드 작성
  • 안드로이드 프레임워크에 종속성이 있는 테스트
  • 실제 안드로이드 기기나 에뮬레이터에서 실행되는 테스트
  • Espresso, UIAnimator, Robotium, Appium, Calabash
  • Instrumentation Test 설명 그림

    테스트코드 작성범위 - "무엇을 테스트해야할까?"

    (1) 수정, 변경되는 모든 기능 : 

    코드의 변경으로 인해 영향받는 코드에 대한 테스트 코드 필수

    (2) 로직에 대한 검증 필수 :

    사용자의 요청에 따라 올바른 로직의 수행결과가 View에 올바르게 반영되는가 검증.

    프로세스 간, 모듈 간, 클래스 간의 통용되는 주요 인터페이스에 대한 테스트, MVP 패턴을 적용한다면 Presenter의 로직, MVVM을 적용한다면 ViewModel의 로직 테스트 등

    (3) 뷰와 로직이 섞여있는 경우 :

    테스트 코드를 작성하려면 최대한 관심사를 분리해야합니다.

    모듈 간 결합도가 낮아지도록 하는 DI, 디자인 패턴, 클린 아키텍처 등을 적용해서 테스트가 쉽게 이루어질 수 있도로 코드 작성

    (4) 그럼에도 어쩔 수 없이 섞여있는 경우 :

    View에 어쩔 수 없이 로직이 포함돼 있는 경우 Instrumented Test를 작성하거나 View로부터 로직을 분리하는 리팩터링 후 분리된 로직에 대한 테스트 코드 작성


    테스트 작성 법칙

    • Given : 특정 상황이 주어진다. ex) 잘못된 이메일 입력
    • When : 테스트하려는 특정 액션이 발생. ex) 로그인 버튼 클릭
    • Then : 변화된 상태나 수행되는 행동 검증. ex) 이메일 검증 실패 메시지를 보여준다.

    @Test
    fun 이메일로그인_실패케이스_잘못된이메일형식입력() {
    //Given
    잘못 된 이메일을 입력.

    //When
    이메일 로그인 버튼 클릭.

    //Then
    유효성 검증 실패 & 이메일 검증 오류 메시지 보여줌.
    }

    @Test
    fun test_onClickEmailLogin_FailLogin_EnterIncorrectEmail() {
    `when`(view.getInputEmail()).thenReturn("abc.def@day")
    viewModel.onClickEmailLogin()
    verify(view).showMessageForIncorrectEmail()
    }

    ☝ 테스트 코드 흐름 예시


    그럼 이제 테스트 코드 작성해보자!!


    1. Dependency 추가

    dependencies {

    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.mockito:mockito-inline:2.21.0'

    }
    android {

    testOptions {
    unitTests.returnDefaultValues = true
    }
    }

    안드로이드에서 Unit test 코드를 빌드할 때는 Android SDK 경로에 있는 android.jar이 아니라 android-stubs-src 파일을 참조한다.

    unitTests.returnDefaultValues를 true로 설정하면 테스트코드의 API가 구현되어있지 않을 때 null 또는 0을 리턴하도록한다.

    따라서 안정적으로 테스트가 가능하게 만들어준다.

    모든 것을 Mocking할 수 없기때문에 필요한 것만 Mocking하고 나머지는 기본값을 리턴하도록 만드는 것이 테스트 코드 작성에 용이하다


    2. Unit test 코드 작성

    Example 클래스의 메소드를 테스트해보자.

    class Example {
    fun getId() : Int {
    return 0
    }

    fun getUrl(id: Int): String {
    return ""
    }
    }

    지금은 Example 객체에 의존성이 존재하지 않는다.

    하지만 실제 코드에서는 다른 객체와 의존성이 존재할 수 있다.

    예를 들어, Example이 서버에서 데이터를 가져오는 경우가 있다. 이럴 때는 테스트 서버를 만들어야한다.

    Mockito를 사용하면 Example의 API를 호출할 때, 어떤 값을 리턴할지 정할 수 있다.

    그래서 어떤 값이 항상 리턴된다고 정하는 방법으로 의존성을 없앨 수 있다.

    이러한 방식으로 특정 클래스, 즉 Unit만 Test하는 코드를 작성할 수 있다.


    {module_name}/src/test/java 에 아래와 같이 테스트 코드를 작성한다.

    @RunWith(MockitoJUnitRunner::class)    //1
    class ExampleUnitTest {


    @Test    //2
    fun example1() {
    val example = Mockito.mock(Example::class.java)    //3

    Mockito.`when`(example.getId()).thenReturn(100)    //4
    Mockito.`when`(example.getUrl(100)).thenReturn("https://codechacha.com")    //5

    assertEquals(100, example.getId())    //6
    assertEquals("https://codechacha.com", example.getUrl(example.getId()))    //7
    }

    }

    1) Mockito를 사용하여 테스트 코드를 작성한다는 뜻

    2) 이 메소드를 테스트하겠다는 것을 JUnit에게 알린다.

    3) Example 클래스를 mocking한다.(가짜 객체를 만든다 생각하면 이해가 쉽다.)

    4) Example.getId()가 호출될 때 100을 리턴하도록 한다.

    5) Example.getUrl(100)이 호출될 때 "https://codechacha.com"을 리턴하도록 한다.

    6) Example.getId()가 100을 리턴하는지 확인

    7) Example.getUrl(100)이 "https://codechacha.com"을 리턴하는지 확인


    anyInt(), anyString() 등

    위의 테스트코드 예제에서 Mocking할 때 특정한 값을 넣어주었다.

    anyInt(), anyString() 등을 사용하면 인자로 어떤 값을 전달하는 상관없게 만들 수 있다.

    @Test
    fun example2() {
    val example = Mockito.mock(Example::class.java)

    Mockito.`when`(example.getId()).thenReturn(100)
    Mockito.`when`(example.getUrl(anyInt())).thenReturn("https://codechacha.com")

    assertEquals(100, example.getId())
    assertEquals("https://codechacha.com", example.getUrl(example.getId()))
    }

    이외에도 anyBoolean(), anyDouble(), anyFloat(), anyList() 등이 있다.


    Exception 발생시키기

    어떤 API를 호출했을 때, Exception이 발생하도록 할 수 있다.

    아래 예제는 getUrl(20)을 호출할 때, IllegalStateException 예외가 전달된다.

    @Test
    fun example3() {
    val example = mock(Example::class.java)

    `when`(example.getUrl(anyInt())).thenReturn("https://codechacha.com")
    assertEquals("https://codechacha.com", example.getUrl(10))

    `when`(example.getUrl(20)).thenThrow(IllegalStateException("Exception happened!"))

    try {
    example.getUrl(20)
    fail()
    } catch (e: IllegalStateException) {
    assertEquals("Exception happened!", e.message)
    }

    doReturn(30).`when`(example).getId()
    assertEquals(30, example.getId())
    }


    Verify()

    Verify는 어떤 API가 호출되었는지, 몇번 이상 호출되었는지 확인할 때 사용한다.

    @Test
    fun example4() {
    val example = mock(Example::class.java)
    `when`(example.getId()).thenReturn(100)
    `when`(example.getUrl(100)).thenReturn("https://codechacha.com")

    example.getId()
    example.getId()
    val url = example.getUrl(example.getId())

    verify(example).getUrl(ArgumentMatchers.eq(100))    //1
    verify(example, times(3)).getId()                   //2
    verify(example, atLeast(2)).getId()                 //3
    verify(example, atLeast(1)).getUrl(100)             //4
    }

    1) getUrl(100)이 호출되었는지 확인

    2) getId()가 3번 호출되었는지 확인. (2번 호출되었으면 테스트 fail)

    3) getId()가 최소 2번은 호출되었는지 확인

    4) getUrl(100)이 최소 1번은 호출되었는지 확인

    그 외에 다양한 메소드를 제공합니다.

    times(), atLeast(), atLeastOnce(), atMost()


    Android SDK를 Mocking 및 Unit test

    지금까지의 예제는 직접 구현한 클래스를 Mocking하여 테스트코드를 작성했다.

    이번에는 Android SDK같은 라이브러리로 제공되는 객체를 Mocking하여 테스트 코드를 작성해보자.

    class MainActivity : AppCompatActivity() {
    // Logger for this class.
    private val TAG = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    if(isSampleAppInstalled(packageManager)) {
    Log.d(TAG, "Sample app is installed in the device.")
    }
    }


    /**
    * Called when the "Save" button is clicked.
    */
    fun onSaveClick(view: View?) {
    // Don't save if the fields do not validate.
    if (!mEmailValidator?.isValid!!) {
    mEmailText!!.error = "Invalid email"
    Log.w(TAG, "Not saving personal information: Invalid email")
    return
    }
    // Get the text from the input fields.
    val name = mNameText!!.text.toString()
    val dateOfBirth: Calendar = Calendar.getInstance()
    dateOfBirth.set(mDobPicker!!.year, mDobPicker!!.month, mDobPicker!!.dayOfMonth)
    val email = mEmailText!!.text.toString()
    // Create a Setting model class to persist.
    val sharedPreferenceEntry = SharedPreferenceEntry(name, dateOfBirth, email)
    // Persist the personal information.
    val isSuccess: Boolean = mSharedPreferencesHelper?.savePersonalInfo(sharedPreferenceEntry) == true
    if (isSuccess) {
    Toast.makeText(this, "Personal information saved", Toast.LENGTH_LONG).show()
    Log.i(TAG, "Personal information saved")
    } else {
    Log.e(TAG, "Failed to write personal information to SharedPreferences")
    }
    }

    companion object {
    const val TAG = "MainActivity"

    fun isSampleAppInstalled(pm: PackageManager): Boolean {
    val SAMPLE_APP_PKG = "com.antwhale.sampletest"
    val ris = pm.getInstalledPackages(0)
    Log.d(TAG, "ris : $ris")
    for(ri: PackageInfo in ris) {
    if(ri.packageName == SAMPLE_APP_PKG) {
    return true
    }
    }
    return false
    }
    }
    }

    ☝ 위의 코드는 MainActivity에서 패키지명 "com.antwhale.sampletest"인 앱이 설치되어있는지 확인하는 내용이다.

    이 코드에서 의존성이 있는 부분은 PackageManager이다.

    Mockito를 이용하여 PackageManager가 항상 "com.antwhale.sampletest" 앱이 설치되어있다고 리턴하도록 만들자.


    마찬가지로 {module_name}/src/test/java 에 아래와 같이 테스트 코드를 작성한다.

    @RunWith(MockitoJUnitRunner::class)
    class PackageManagerTest {
    @Test
    fun sample_app_is_installed() {
    /*val pi = Mockito.mock(PackageInfo::class.java)
    pi.packageName = "com.antwhale.sampletest"*/

    val pi = PackageInfo()    //1
    pi.packageName = "com.antwhale.sampletest"
    val installedApps: List<PackageInfo> = listOf(pi)    //2

    val pm = Mockito.mock(PackageManager::class.java)    //3
    Mockito.`when`(pm.getInstalledPackages(0)).thenReturn(installedApps)    //4

    assertTrue(MainActivity.isSampleAppInstalled(pm))    //5
    }


    }

    1) PackageInfo 객체를 만든다

    2) PackageInfo의 packageName에 "com.antwhale.sampletest"를 설정하여 리스트에 담아준다.

    3) PackageManager의 Mock 객체를 만든다.

    4) getInstalledPackages(0)이 호출될 때 위에서만든 리스트를 리턴한다.

    5) 테스트할 API를 호출해서 true가 리턴되는지 확인한다.


    ✋ 만약 아래 에러가 발생한다면 위에서 소개한 returnDefaultValues = true 로 설정한다

    java.lang.RuntimeException: Method toString in android.content.pm.PackageInfo not mocked. See http://g.co/androidstudio/not-mocked for details.


    참고:

    https://yoon-dailylife.tistory.com/114

    https://codechacha.com/ko/android-mockito-for-test/

    Share:
    Read More