본문 바로가기

기타/안드로이드

Tip Time App - local & instrumentation test

Tip Time App - local & instrumentation test

자동 테스트 작성  |  Android 개발자  |  Android Developers

 

자동 테스트 작성  |  Android 개발자  |  Android Developers

이 Codelab에서는 자동 테스트와 이 테스트가 중요한 이유 및 작성 방법을 알아봅니다.

developer.android.com

위 페이지를 참고하여 프로그램 개발에 필수적인 테스트, 그 중에서도 자동테스트를 경험해보자

 

 

개요

프로그램 개발을 할 때 마지막에는 테스트를 해본다는 것을 들어본 적이 있을 것이다

테스트에는 자동테스트와 수동테스트가 있고

우리가 관심을 가지는 자동테스트는 local 테스트와 instrumentation 테스트가 있다

 

각각 대충 코드로직 테스트와 UI기능 테스트라고 이해하면 되는데

어떻게 해야 테스트를 진행할 수 있는지 알아보도록 하자

 

 

Local(Logic) Test

Android Studio에서 프로젝트 트리를 보면 괄호 안에 androidTest와 test라고 되어있는 친구들을 볼 수 있다

여기서 test가 코드로직 테스트, androidTest가 UI기능 테스트이다

 

logic 테스트 구조 이해하기

일단 test에 있는 예시 테스트를 확인해보자

class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}

이런 친구가 있을텐데 차근차근 설명해보겠다

@Test annotation을 달아놓은 친구들만 테스트가 가능하다

 

그리고 assertion이라는 개념이 나오는데

대충 '~어야 한다'라는 뜻이다

그러니까 저기 assertEquals는 '얘내 둘이 같아야만 한다'

뭐 이런 거다

 

assertion에는 assertTrue, assertNull 등이 있다

 

그러니깐 사실 로직테스트는 별 거 없다

테스트하고 싶은 로직이 있으면 그 로직을 이용해 어떤 값을 구한 다음에

assertion으로 확인하면 끝이다

중요한 포인트는 값을 구하는 로직을 검사하는게 아니라

input과 output을 체크해서 예상된 값이 나오면 잘 되는 거라고 판단한다는 것이다

코드 로직의 내부가 엉망이어도 input에 대한 output만 잘 나오면 장땡이라는 것

당연히 테스트에 이용하는 input, output 쌍이 많을 수록 정확해진다 

 

Tip Time App logic test 해보기

그러면 이제 Tip 계산기의 calculateTip함수의 로직을 검사해보자

함수가 private으로 되어있으면 test 코드도 접근을 못하기 때문에 internal로 바꿔주자

 

그리고 다음과 같은 함수를 만들자

class ExampleUnitTest {
    @Test
    fun myFirstTest(){
        val amount = 100.0
        val tipPercent = 10.0
        val roundUp = false
        val result = calculateTip(amount, tipPercent, roundUp)
        val expectedResult = "₩10"
        assertEquals(result, expectedResult)
    }
}

총 금액이 100, 팁 비율이 10%, 반올림이 없이 계산을 하면 총 팁의 값이 10이 나와야 할 것이다

솔직히 너무 간단해서 이해가 다 됐을 거라고 생각한다

(calculateTip의 결과값이 NumberFormat.getcurrencyInstance().format(tip)인데, 원화값으로 나오고 있었나보다

그래서 expectedResult가 ₩10인 것..

지금까지 몰랐음.. 이게 테스트의 힘??)

 

 

 

그러면 이제 함수 옆에 있는 화살표를 클릭해 테스트를 진행하자

 

그러면 이제 테스트가 잘 됐다고 뜰 것이다

 

이것으로 local test 해보기는 끝

 

 

 

 

Instrumentation(UI) Test

UI, instrumentation 테스트는 androidTest라고 되어있는 폴더에서 진행할 수 있다

 

instrumentation test 이해하기

local, 로직 테스트처럼 일단 예시로 만들어져 있는 테스트를 참고해보자

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.tiptime", appContext.packageName)
    }
}

대충 이렇게 생겼는데.. 일단 RunWith 빼고는 local test와 크게 다르지 않아보인다

RunWith annotation은 테스트를 할 때 확장기능을 이용하기 위해 쓴다고 한다

저 뒤에 명시된 JUnit4 뭐시기를 쓴다는 거 같음

JUnit은 Java에서 테스트를 할 때 쓰는 프레임워크라고 한다

 

그런데 저 InstrumentationRegistry를 확인해보면 이제는 사용하지 않는(deprecated) 녀석이라고 하고 있고

공식문서에서 알려주는 내용과 많이 다르기 때문에

이 기본 코드는 진짜 그냥 한 번 보기만 하고 넘어가야겠다

 

tip 계산기 instrumentation test 해보기

일단 instrumentation 테스트에 대한 완성코드는 다음과 같다

package com.example.tiptime

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput
import com.example.tiptime.ui.theme.TipTimeTheme
import org.junit.Rule
import org.junit.Test

class TipUITests {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun calculate_20_percent_tip() {
        composeTestRule.setContent {
            TipTimeTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    TipTimeScreen()
                }
            }
        }
        composeTestRule.onNodeWithText("Cost of Service").performTextInput("10")
        composeTestRule.onNodeWithText("Tip (%)").performTextInput("20")
        composeTestRule.onNodeWithText("Tip amount: $2.00").assertExists()

    }
}

하나하나 짚어보려고 했는데...

솔직히 검색해도 잘 안나오고 내용 이해도 잘 안되기 때문에 지금은 기본구조까지를 그냥 외우기로 했다

class JustTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun testFun() {
        composeTestRule.setContent {
            testTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    testUI()
                }
            }
        }
    }
}

딱 이런 구조까지만 외우고 나머지를 이해해보자

 

createComposeRule로 만들어지는 객체가 무엇인지를 잘 모르겠지만

onNodeWithText를 통해서 어떤 node를 찾을 수 있는 것으로 보인다

html, javascript 할 때 getElementById()와 비슷한 녀석으로 보인다

 

간단히 말해 저 텍스트를 포함한 composable의 정보(=context?)를 가져오고

performTextInput, 그러니까 글자를 입력하겠다는 것이다

그렇게 서비스 가격과 팁의 비율을 입력한 뒤

계산된 팁의 가격을 찾아 assertExist, 값이 존재하면 테스트가 참이 되는 것이다

 

사실 구조가 처음봐서 생소하고 어색해서 그렇지

내용 자체는 어려운 게 아니다