Android 자동 테스트란...?

정대영·2025년 3월 25일

Android Test

목록 보기
2/2
post-thumbnail

테스트 코드가 좋은 건 알겠어! 근데... 굳이 작성해야돼?

프로젝트를 만들어 가면서 테스트 코드가 중요하다는 사실은 누구나 알 것 이다.
테스트 코드를 통해 오류를 빠르게 검출하고 해결할 수 있고, 테스트 코드가 잘 짜여진 코드는 대부분 가독성이 쉬운 듯 하다.(아닐 수 있음)

나는 지금까지 오류가 발생하거나 기능을 테스트 하기 위해 앱 전체를 빌드하고 Log를 찍고 확인하는 과정을 거쳤다. 작은 규모의 프로젝트라면 Log를 찍어 기능을 테스트하는 시간이 적게 걸리겠지만, 큰 프로젝트의 경우 테스트 코드를 짜는 시간이 Log를 찍어 기능을 테스트하는 시간보다 적게 걸릴 것이다.

간단한 예를 설명하자면 버스 정류장을 경유하는 모든 버스들을 조회하는 기능을 테스트 하고 싶은 상황이다.
case 1) 앱 전체 빌드 --> 로그인 --> 버스 조회
case 2) 테스트 코드 짜는 시간 ------------------> 테스트

위의 예시 처럼 간단한 기능을 하는 경우 테스트 코드를 짜는 것은 비효율적일 수 있다.
그러나 버스 정류장을 경유하는 모든 버스를 조회하고 버스를 선택하고 저장하고 저장한 버스를 나중에 확인할 수 있는 기능을 테스트 해보자.
case 1) 앱 전체 빌드 --> 로그인 --> 버스 조회 --> 버스 선택 --> 버스 저장 --> 저장한 버스 조회
case 2) 테스트 코드 짜는 시간 ------------------> 테스트

위의 예시처럼 많은 과정을 거쳐야 된다면 테스트 코드를 작성하고 테스트 하는 시간이 훨씬 적게 들 것이다.

특히 안드로이드같은 경우 앱을 빌드하는 시간이 생각보다 소요되기 때문에 큰 프로젝트에서는 테스트 코드가 필요하다!

자동 테스트와 수동 테스트

자동 테스트는 개발자가 작성한 코드의 또 다른 부분이 올바르게 작동하는지 확인하는 코드이다.

수동 테스트는 기기와 직접 상호작용하는 사람이 실행한다. ex) 앱에서 직접 터치했을 때 발생하는 이벤트를 확인, 오류가 날 경우 Log를 찍어서 확인
앱의 규모가 커지면 수동 테스트를 할 때 자동 테스트보다 훨씬 많은 노력이 필요하다.
자동 테스트수동 테스트보다 더 정확하고 생산성을 최적화시킬 수 있다.

로컬 테스트와 계측 테스트

프로젝트를 하다보면 src폴더 아래에 androidTest, main, test 폴더가 있는 것을 본적 있을 것이다.
androidTest 폴더에 테스트 코드를 작성하고 테스트하는 것을 계측 테스트(Instrumented Test)
test 폴더에 테스트 코드를 작성하고 테스트하는 것을 로컬 테스트라고 한다.

  • 로컬 테스트
    로컬 테스트는 소수의 코드를 직접 테스트하여 제대로 작동하는지 확인하는 자동 테스트의 유형이다. 함수, 클래스, 속성 등을 테스트할 수 있다. 기기나 에뮬레이터가 없는 개발 환경(개발자 컴퓨터)에서 실행되기 떄문에 빠른 속도로 테스트 가능하다는 장점이 있다.
    컴퓨터 리소스의 오버헤드도 매우 낮아서 제한된 리소소에서 빠르게 실행할 수 있다.
    로컬에서만 돌리기 때문에 context를 얻을 수 없다.
    dependencies에서 testImplementation으로 라이브러리를 추가한다.

    특징: 개발자 컴퓨터에서 테스트
    장점: 빠른 속도로 테스트
    단점: context가 필요로 하는 테스트가 불가능

  • 계측 테스트(Instrumented Test)
    Android 개발의 경우 계측 테스트는 UI 테스트이다. 계측 테스트를 사용하면 Android API와 플랫폼 API 및 서비스에 종속된 앱 일부를 테스트할 수 있다.
    계측 테스트를 실행하면 테스트 코드는 실제 Android 앱처럼 자체 APK(Android Application Package)로 빌드 된다. 빌드된 테스트 APK는 기기나 애뮬레이터에 설치되고 실행된다.
    계측 테스트는 에뮬레이터 또는 실제 안드로이드 기기 환경에서 테스트 하기 때문에 로컬 테스트보다 느리다.
    실제 기기에서 사용하기 때문에 context를 사용이 가능하다.
    dependencies에서 androidTestImplementation으로 라이브러리를 추가한다.

    특징: 기기나 에뮬레이터에서 테스트
    장점: UI 테스트 가능, context 사용 가능
    단점: 느린 속도로 테스트

로컬 테스트와 계측 테스트 코드 살펴보기

계측 테스트

계측 테스트는 앱과 UI의 실제 인스턴스를 테스트한다. Compose로 빌드된 앱의 모든 계측 테스트를 작성하기 전에 이 작업을 실행해야 한다.

  1. createComposeRule() 메서드의 결과를 저장할composeTestRule 변수를 만들고 Rule 주석을 추가한다.
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class TipUITests {

   @get:Rule
   val composeTestRule = createComposeRule()
}
  1. calculate_20_percent_tip() 메서드를 만들고 @Test 주석을 추가한다.
import org.junit.Test

@Test
fun calculate_20_percent_tip() {}
  1. 함수 본문에서 composeTestRule.setContent() 함수를 호출, composeTestRule의 UI 콘텐츠가 설정된다.

  2. 함수의 람다 본문에서 TipTimeLayout() 함수를 호출

import com.example.tiptime.ui.theme.TipTimeTheme

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
           TipTimeLayout()
        }
    }
}

이제 UI 콘텐츠의 설정이 완료되었으므로 앱의 UI 구성요소와 상호작용하는 명령을 작성할 수 있다.

  1. UI 구성요소는 composeTestRule을 통해 노드로 액세스할 수 있다. 일반적인 방법으로 onNodeWithText() 메서드를 사용하여 특정 텍스트가 포함된 노드에 액세스하는 것이다.
    onNodeWithText() 메서드를 사용하여 청구 금액에 관한 TextField 컴포저블에 액세스 한다.
import androidx.compose.ui.test.onNodeWithText

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
    composeTestRule.onNodeWithText("Bill Amount")
}

이제 performTextInput() 메서드를 호출하고 입력하려는 텍스트를 전달하여 TextField 컴포저블을 채울 수 있습니다.

  1. amout의 Textfield10으로 채우고 tip의 OutlinedTextField20으로 채운다.
import androidx.compose.ui.test.performTextInput

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
    composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
	composeTestRule.onNodeWithText("Tip Percentage").performTextInput("20")
}

TextField 컴포저블에 텍스트를 채웠으니 결과를 assert 메서드롤 확인한다. Text 컴포저블은 다음과 같이 표시되어야 한다. Tip Amount: $2.00

import java.text.NumberFormat

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            Surface (modifier = Modifier.fillMaxSize()){
                TipTimeLayout()
            }
        }
    }
   composeTestRule.onNodeWithText("Bill Amount")
      .performTextInput("10")
   composeTestRule.onNodeWithText("Tip Percentage").performTextInput("20")
   val expectedTip = NumberFormat.getCurrencyInstance().format(2)
   composeTestRule.onNodeWithText("Tip Amount: $expectedTip").assertExists(
      "No node with this text was found."
   )
}
  • UI

로컬 테스트

// MainActivity.kt내의 코드
@VisibleForTesting
internal fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
    var tip = tipPercent / 100 * amount
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    return NumberFormat.getCurrencyInstance().format(tip)
}

내가 내야할 금액 amout에서 tip의 퍼센트를 지정했을 때 받을 수 있는 금액을 구하는 함수를 테스트 해보자.
roundUp는 받을 수 있는 금액의 반올림 여부를 체크하는 매개변수이다.
@VisibleForTesting는 메서드가 공개되지만 테스트 목적으로만 공개된다고 사용자에게 표시하는 용도로 쓰인다.

import org.junit.Test

class TipCalculatorTests {
	@Test
    fun calculateTip_20PercentNoRoundup() {
        val amount = 10.00
        val tipPercent = 20.00
        val expectedTip = NumberFormat.getCurrencyInstance().format(2)
        val actualTip = calculateTip(amount = amount, tipPercent = tipPercent, false)
        assertEquals(expectedTip, actualTip)
    }
}

테스트를 하는 함수에 @Test를 붙여야 한다.
참고로 @Test 주석은 org.junit.Test에서 가져온다.

읽기 쉬운 테스트를 작성하는 전략

Given, When, Then 방식을 사용하는 것이다.

  • Given(주어진 것)
    테스트할 데이터(객체나 상태)를 준비한다.
    ex) "name", listOf("Oil", "Banana", "Apple"), isAutoLogin = true
  • When(언제)
    테스트할 메서드를 호출한다.
    ex) login()
  • Then(그런 다음)
    테스트한 결과값과 예상되는 결과값을 비교한다.(assert를 이용한 코드)
@Test
//  모란 고개에 240번 버스가 경유 하는지 테스트
fun readAllBus_inputCityNameAndBusStopNodeId_containRouteNumber30() = testScope.runTest {

	// give
	val cityName = "성남시"
	val busStopId = "GGB204000087" // 모란 고개

	// when
	val result = repository.readAllBus(cityName = cityName, busStopId = busStopId)

	// then
	assertNotNull(result.find { it.name == "240" })
}

테스트 메서드 네이밍 규칙

테스트 메서드 이름을 정할 때, 아래 순서로 조합하면 된다
[테스트 대상]_[동작 또는 입력]_[예상 결과]
ex) login_validUser_success

  • login(테스트 대상)
    테스트하는 메서드의 이름을 적으면 된다.
    ex) login() 이라는 메서드 이름

  • validUser(입력값이나 동작)
    테스트 대상에 어떤 상황이나 입력을 줄 것인지를 의미한다.
    ex) valiUser는 유효한 사용자 정보라는 의미

  • success(예쌍 결과)
    테스트를 실행했을 때 기대하는 결과
    ex) success는 로그인이 성공했다는 의미


테스트 대상동작(입력값)예상 결과테스트 메서드 이름
login 메서드유효한 사용자 정보 입력로그인 성공login_validUser_success
calculateDiscount 메서드VIP 고객 입력20% 할인 적용calculateDiscount_vipUser_returnsTwentyPercent
fetchData 메서드서버 연결 불가예외 발생fetchData_noConnection_throwsException

reference

자동테스트의 로컬 테스트, 계측 테스트 codelab
https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-basics#5
https://developer.android.com/training/testing/fundamentals?hl=ko
https://blog.naver.com/nakim02/222416047403

profile
매일 그리고 꾸준히, 성장하는 개발자가 되자

0개의 댓글