UI 테스트는 모바일 애플리케이션 사용자 인터페이스 요소의 표시와 상호작용의 정확성을 검증합니다 — 버튼, 텍스트 필드, 리스트 및 네비게이션 컴포넌트 등이 포함됩니다. 비즈니스 로직을 검사하는 단위 테스트와 달리, UI 테스트는 사용자 작업을 시뮬레이션합니다: 탭, 스와이프, 텍스트 입력 그리고 인터페이스 응답을 검증합니다. Android Developers, 2024의 연구에 따르면, UI 테스트는 중요한 사용자 시나리오의 70%를 다루고 논리적 검사로는 탐지할 수 없는 레이아욳 결함을 발견할 수 있습니다.
주요 포인트
UI 테스트는 테스트 코드가 실제 사용자처럼 애플리케이션의 그래픽 인터페이스와 상호작용하는 자동화 검증 유형입니다. 테스트는 화면에서 요소(버튼, 텍스트 필드, 리스트)를 찾고, 그에 대한 작업을 수행하며 기대되는 인터페이스 응답을 검증합니다. 예를 들어, 올바르지 않은 비밀번호를 입력한 후, UI 테스트는 올바른 텍스트의 오류 메시지가 화면에 나타나는지 검사합니다.
UI 테스트와 다른 유형의 자동화 간의 주요 차이점은 애플리케이션의 내부 API가 아닌 운영체제의 Accessibility 계층을 통해 작동한다는 것입니다. 이는 UI 테스트가 사용자와 스크린 리더가 보는 것과 똑같이 인터페이스를 보다는 뜻입니다. 이를 통해 UI 테스트는 기능성만 아니라 요소의 접근성(WCAG 요구사항 준수)도 검증할 수 있습니다.
JetBrains Developer Ecosystem 2023 조사에 따르면, 58%의 모바일 팀이 CI/CD 파이프라인에서 UI 테스트를 사용하고 있습니다. 상용 프로젝트에서 평균 UI 테스트 케버리지는 애플리케이션 화면의 30–40%입니다. UI 테스트가 있는 프로젝트는 인터페이스 충돌과 관련된 앱 스토어의 부정 리뷰가 25% 더 적습니다.
UI 테스트와 단위 테스트 간의 주요 차이점은 추상화 레벨입니다. 단위 테스트는 Android 또는 iOS 프레임워크에서 분리된 개별 클래스와 함수를 다룹니다. 이들은 애뮬레이터를 시작하지 않고 JVM에서(안드로이드의 경우) 실행되며 밀리초 단위로 완료됩니다. UI 테스트는 실제 기기나 애뮬레이터에서 실행되고, 시스템 서비스와 상호작용하며 시나리오당 수초에서 수분이 걸립니다.
테스트의 대상 시청거리도 다릅니다. UI 테스트는 종단 사용자 시나리오(회원 가입, 주문 접수, 검색)를 검증합니다. 단위 테스트는 비즈니스 로직(계산, 검증, 데이터 변환)을 다룹니다. UI 테스트는 세금 계산의 정확성을 검증하지 않습니다 — 총 금액이 화면에 표시되는지를 검증합니다. 계산 자체는 단위 테스트로 검증됩니다.
Google Testing Blog (2020)에 따르면, 프로젝트에서 가장 이상적인 테스트 비율은 테스트 피라미드 규칙을 따릅니다: 70% 단위 테스트, 20% 통합 테스트, 10% UI 테스트. UI 테스트 중심으로 이 비율을 위반하면 실행 시간이 늘어나고 테스트 스위트가 연약해집니다. UI 테스트는 화면 레이아웃 변경에 민감하기 때문입니다.
Android에서 주도적인 프레임워크는 Espresso입니다 — AndroidX Test에 포함된 Google의 라이브러리입니다. Espresso는 자동으로 UI 스레드와 동기화되어, 다음 검증을 수행하기 전에 애니메이션과 백그라운드 작업이 완료될 때까지 기다립니다. Jetpack Compose에서는 기존 뷰 식별자 대신 시맨틱 노드를 통해 작동하는 Compose UI Test 확장이 사용됩니다.
iOS에서 주요 도구는 XCUITest로, Xcode의 일부입니다. 테스트는 Swift로 작성되며 요소를 찾기 위해 Accessibility 식별자를 사용합니다. XCUITest는 레코드 기능을 통한 테스트 녹화와 xcodebuild를 통한 CI 시스템 과의 통합을 지원합니다. 크로스 플랫폼 프로젝트에서는 WebDriver 프로토콜에 기반한 Appium이 사용되며, 최소한의 코드 변경으로 Android와 iOS에서 동일한 테스트를 실행할 수 있습니다.
Espresso는 onView와 리소스 ID 식별자를 통해 기존의 뷰 시스템으로 작동합니다. Compose UI Test는 시맨틱 계층을 사용하여 테스트가 뷰 계층구조에 더 적게 의존하도록 합니다. 예를 들어, Espresso에서 버튼 찾기: onView(withId(R.id.submit)), Compose에서: onNodeWithTag(“submit”). Compose 테스트는 재구성을 자동으로 처리하며 명시적인 대기 상태 대기가 필요하지 않습니다.
XCUITest는 시작 지점으로 XCUIApplication을 사용합니다. 각 인터페이스 요소는 Accessibility 특성을 통해 찾습니다: 프로그램적 접근을 위한 accessibilityIdentifier와 VoiceOver를 위한 accessibilityLabel. 이 프레임워크는 Xcode의 레코드 기능을 통한 테스트 녹화를 지원합니다 — 개발자가 시뮬레이터에서 작업을 수행하면 Xcode가 테스트 코드를 생성합니다. 준비된 테스트는 xcodebuild test를 통해 실행됩니다.
Appium은 WebDriver 프로토콜에 기반하며 Java, Python, JavaScript 등 다양한 언어를 지원합니다. 요소 검색 전략에는 id, xpath, class name 그리고 accessibility id가 포함됩니다. Appium은 서버 설치와 Desired Capabilities(platformName, deviceName, appPackage)의 구성이 필요합니다. 대체 방안으로는 YAML 시나리오를 사용하고 테스트 코드 컴파일이 필요하지 않은 Maestro가 있습니다.
세 가지 다른 프레임워크에서 동일한 시나리오(애플리케이션에 로그인)에 대한 UI 테스트를 살펴보겠습니다: Android용 Espresso, iOS용 XCUITest, 그리고 크로스 플랫폼 접근법용 Appium. 시나리오: 로그인과 비밀번호를 입력하고, 로그인 버튼을 누르고, 환영 메시지가 표시되는지 확인합니다.
Espresso 테스트는 onView를 사용하여 식별자로 요소를 찾고, perform으로 작업을 수행합니다. isDisplayed 매치어가 있는 check 메소드는 요소가 화면에 표시되는지 확인합니다.
@RunWith(AndroidJUnit4::class)
class LoginUiTest {
@Rule
@JvmField
val composeTestRule = createComposeRule()
@Test
fun login_withValidCredentials_showsWelcome() {
composeTestRule
.onNodeWithTag("emailField")
.performTextInput("user@example.com")
composeTestRule
.onNodeWithTag("passwordField")
.performTextInput("secret123")
composeTestRule
.onNodeWithTag("loginButton")
.performClick()
composeTestRule
.onNodeWithText("환영합니다, 사용자님!")
.assertIsDisplayed()
}
}
XCUITest는 Accessibility 식별자를 통해 인터페이스 요소에 접근하기 위해 XCUIApplication을 사용합니다. tap()과 exists 메소드는 상호작용과 검증을 제공합니다.
class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
continueAfterFailure = false
app.launch()
}
func testLogin_withValidCredentials_showsWelcome() {
app.textFields["emailField"].tap()
app.textFields["emailField"].typeText("user@example.com")
app.secureTextFields["passwordField"].tap()
app.secureTextFields["passwordField"].typeText("secret123")
app.buttons["loginButton"].tap()
XCTAssertTrue(app.staticTexts["Welcome, User!"].exists)
}
}
첫 번째 원칙 — 요소를 찾기 위해 텍스트 레이블 대신 Accessibility 식별자를 사용하세요. 버튼의 텍스트는 로컬라이제이션 시 변경될 수 있지만, 식별자는 그대로 유지됩니다. Android에서는 contentDescription 속성, iOS에서는 accessibilityIdentifier입니다. 이 접근 방식은 테스트가 인터페이스 언어에서 독립적이도록 하고, 커피라이팅이 변경될 때 유지보수 비용을 줄임니다.
sleep()과 고정 지연을 피하세요 — 프레임워크의 내장 대기 메커니즘을 사용하세요. Espresso는 자동으로 애니메이션과 백그라운드 작업의 완료를 기다립니다. XCUITest는 타임아웃과 함께 XCTAssertTrue를 제공합니다. 명시적인 일정 중단은 특히 CI 환경에서 늘린 기기에서 테스트를 더 느리고 불안정하게 만듭니다.
중요도에 따라 테스트를 그룹화하세요: 스모크 테스트(3–5개의 주요 시나리오)는 모든 커미트에서 실행되고, 전체 UI 테스트 스위트는 릴리스 전에 실행됩니다. Google Testing Blog (2022)에 따르면, CI에서 30분이 거리는 UI 테스트는 실행 빈도가 40% 감소하여, 이른 리그레션 감지 도구로서의 효과성이 떨어집니다.
UI 테스트에는 몇 가지 한계가 있습니다. 레이아웃 변경에 대한 민감성: 식별자, 계층구조 또는 요소 유형이 바뀌면 기능이 그대로인 경우에도 테스트가 실패합니다. 해결 방법은 요소 선택자를 별도 클래스에 중앙화하는 Page Object 패턴을 사용하는 것입니다. 레이아웃이 변경되면 수십 개의 테스트 대신 하나의 Page Object 파일만 수정하면 됩니다.
실행 시간: 실제 기기나 애뮬레이터에서의 실행은 단위 테스트보다 10–50배 더 길게 걸립니다. 해결 방법은 Firebase Test Lab 또는 AWS Device Farm을 통해 여러 기기에서 UI 테스트를 병렬로 실행하는 것입니다. 불안정성(flakiness)은 애니메이션, 네트워크 지연 또는 애뮬레이터 상태로 인한 CI 실행의 일반적인 문제입니다. 불안정성을 해결하기 위해 실패한 테스트의 자동 재시도와 각 테스트 시나리오의 안정성 분석이 사용됩니다.
자주 묻는 질문
평균 화면에서는 3–5개의 UI 테스트로 같니다: 해피 패스, 오류 검증, 빈 상태, 화면 회전 및 Accessibility 검사. 여러 상태를 가진 복잡한 화면 — 주문 양식, 설정 — 주요 시나리오의 완전한 커버리지를 위해 10–15개의 테스트가 필요할 수 있습니다.
네, Appium과 Maestro는 두 플랫폼 모두에서 동일한 시나리오를 실행할 수 있습니다. 그러나 네이티브 프레임워크( Espresso와 XCUITest)는 더 나은 안정성, 속도 그리고 WebDriver 프록시를 통해서는 사용할 수 없는 플랫폼 구족 기능에 대한 액세스를 제공합니다.
Compose에서는 시맨틱 매치어가 있는 Compose UI Test 라이브러리를 사용합니다: onNodeWithText, onNodeWithTag, onNodeWithContentDescription. Compose의 시맨틱 계층은 뷰 계층구조를 추상화하여 기존 뷰 시스템용 Espresso보다 테스트가 더 직렬하지 않도록 합니다.
기본 UI 테스트 실행은 CI에서 애뮬레이터로 수행됩니다 — 빠르고 저렴합니다. 릴리스 전 최종 검증은 물리적인 기기에서 Firebase Test Lab을 통해 수행하여 실제 하드웨어 특성(다양한 해상도, OS 버전 및 성능)을 고려하는 것이 좋습니다.
여러 기기에서 병렬 실행을 사용하고, 개발자 옵션을 통해 애뮬레이터의 애니메이션을 비활성화하며, 모듈형 테스트 아키텍처를 구축하고, 모든 커미트에서 스모크 스위트를 실행하며, 전체 회귀 테스트는 일정에 따라 또는 릴리스 전에 실행합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.