대규모 테스트 안정성

모바일 애플리케이션과 프레임워크의 비동기적 특성으로 인해 안정적이고 반복 가능한 테스트를 작성하기 어려운 경우가 많습니다. 사용자 이벤트가 삽입되면 테스트 프레임워크는 앱이 이에 대한 반응을 완료할 때까지 기다려야 합니다. 이는 화면의 일부 텍스트 변경에서 활동의 완전한 재생성에 이르기까지 다양할 수 있습니다. 테스트에 확정적인 동작이 없으면 불안정합니다.

Compose나 Espresso와 같은 최신 프레임워크는 테스트를 염두에 두고 설계되어 다음 테스트 작업이나 어설션 전에 UI가 유휴 상태가 될 것이라는 확실한 보장이 있습니다. 이것이 동기화입니다.

동기화 테스트

데이터베이스에서 데이터를 로드하거나 무한 애니메이션을 표시하는 등 테스트에 알려지지 않은 비동기 또는 백그라운드 작업을 실행하면 여전히 문제가 발생할 수 있습니다.

테스트 통과 전에 앱이 유휴 상태인지 확인하는 루프를 보여주는 흐름 다이어그램
그림 1: 동기화 테스트

테스트 모음의 안정성을 높이려면 Espresso 유휴 리소스와 같이 백그라운드 작업을 추적하는 방법을 설치할 수 있습니다. 또한 유휴 상태를 쿼리할 수 있거나 동기화를 개선하는 버전 테스트용으로 모듈을 대체할 수 있습니다(예: 코루틴의 경우 TestDispatcher 또는 RxJava의 경우 RxIdler).

동기화가 고정된 시간 대기를 기반으로 할 때 테스트 실패를 보여주는 다이어그램
그림 2: 테스트에서 sleep을 사용하면 테스트 속도가 느려지거나 불안정해집니다.

안정성 개선 방법

대규모 테스트는 앱의 여러 구성요소를 테스트하기 때문에 많은 회귀를 동시에 포착할 수 있습니다. 일반적으로 에뮬레이터나 기기에서 실행되므로 충실도가 높습니다. 대규모 엔드 투 엔드 테스트는 포괄적인 범위를 제공하지만 가끔 실패하기 쉽습니다.

불안정성을 줄이기 위해 취할 수 있는 기본적인 조치는 다음과 같습니다.

  • 기기를 올바르게 구성
  • 동기화 문제 방지
  • 재시도 구현

Compose 또는 Espresso를 사용하여 대규모 테스트를 만들려면 일반적으로 활동 중 하나를 시작하고 사용자가 하는 것처럼 탐색하여 어설션 또는 스크린샷 테스트를 사용하여 UI가 올바르게 작동하는지 확인합니다.

UI Automator와 같은 다른 프레임워크를 사용하면 시스템 UI 및 다른 앱과 상호작용할 수 있으므로 더 큰 범위를 허용합니다. 그러나 UI Automator 테스트에는 더 많은 수동 동기화가 필요할 수 있으므로 신뢰성이 떨어지는 경향이 있습니다.

기기 구성

먼저 테스트의 안정성을 개선하려면 기기의 운영체제가 테스트 실행을 예기치 않게 중단하지 않도록 해야 합니다. 시스템 업데이트 대화상자가 다른 앱 위에 표시되거나 디스크의 공간이 부족한 경우를 예로 들 수 있습니다.

기기 팜 제공업체가 기기와 에뮬레이터를 구성하므로 일반적으로 별도의 조치를 취하지 않아도 됩니다. 그러나 특별한 경우에는 자체 구성 지시문이 있을 수 있습니다.

Gradle 관리 기기

에뮬레이터를 직접 관리하는 경우 Gradle 관리 기기를 사용하여 테스트 실행에 사용할 기기를 정의할 수 있습니다.

android {
  testOptions {
    managedDevices {
      localDevices {
        create("pixel2api30") {
          // Use device profiles you typically see in Android Studio.
          device = "Pixel 2"
          // Use only API levels 27 and higher.
          apiLevel = 30
          // To include Google services, use "google".
          systemImageSource = "aosp"
        }
      }
    }
  }
}

이 구성을 사용하면 다음 명령어로 에뮬레이터 이미지를 만들고 인스턴스를 시작하고 테스트를 실행한 다음 종료할 수 있습니다.

./gradlew pixel2api30DebugAndroidTest

Gradle 관리 기기에는 기기 연결 끊김 및 기타 개선 사항이 발생할 경우 재시도하는 메커니즘이 포함되어 있습니다.

동기화 문제 방지

백그라운드 또는 비동기 작업을 실행하는 구성요소는 UI가 준비되기 전에 테스트 문이 실행되어 테스트 실패를 일으킬 수 있습니다. 테스트 범위가 넓어질수록 불안정해질 가능성이 높아집니다. 이러한 동기화 문제는 테스트 프레임워크가 활동 로드가 완료되었는지 아니면 더 기다려야 하는지 추론해야 하므로 불안정성의 주요 원인입니다.

솔루션

Espresso의 유휴 리소스를 사용하여 앱이 사용 중일 때를 나타낼 수 있지만, 특히 매우 큰 엔드 투 엔드 테스트에서는 모든 비동기 작업을 추적하기가 어렵습니다. 또한 테스트 중인 코드를 오염시키지 않고 유휴 리소스를 설치하기가 어려울 수 있습니다.

활동의 사용 여부를 추정하는 대신 특정 조건이 충족될 때까지 테스트가 대기하도록 할 수 있습니다. 예를 들어 특정 텍스트 또는 구성요소가 UI에 표시될 때까지 기다릴 수 있습니다.

Compose에는 다양한 매처를 기다리는 ComposeTestRule의 일부로 테스트 API 모음이 있습니다.

fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilExactlyOneExists(matcher: SemanticsMatcher,  timeout: Long = 1000L)

fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)

그리고 불리언을 반환하는 함수를 사용하는 일반 API:

fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit

사용 예:

composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>

재시도 메커니즘

불안정한 테스트는 수정해야 하지만, 실패를 야기하는 조건이 너무나도 발생 가능성이 낮아 재현하기 어려운 경우가 있습니다. 항상 불안정한 테스트를 추적하고 수정해야 하지만 재시도 메커니즘을 사용하면 테스트가 통과할 때까지 여러 번 테스트를 실행하여 개발자 생산성을 유지할 수 있습니다.

다음과 같은 문제를 방지하려면 여러 수준에서 재시도해야 합니다.

  • 기기 연결 시간이 초과되거나 연결이 끊김
  • 단일 테스트 실패

재시도 설치 또는 구성은 테스트 프레임워크 및 인프라에 따라 다르지만 일반적인 메커니즘은 다음과 같습니다.

  • 테스트를 여러 번 재시도하는 JUnit 규칙
  • CI 워크플로의 재시도 작업 또는 단계
  • Gradle 관리 기기와 같이 응답하지 않을 때 에뮬레이터를 다시 시작하는 시스템입니다.