목차
1. 왜 UI 테스트는 Espresso여야 하는가?
소프트웨어 개발에서 테스트는 더 이상 선택이 아닌 필수 과정입니다. 특히 사용자와 직접 상호작용하는 모바일 애플리케이션에서 사용자 인터페이스(UI) 테스트는 제품의 품질과 직결되는 핵심적인 요소입니다. 불안정하거나 예기치 않게 동작하는 UI는 사용자에게 나쁜 경험을 제공하고, 이는 곧 앱의 이탈로 이어질 수 있습니다. Google이 개발한 Espresso는 이러한 안드로이드 UI 테스트를 자동화하여 개발자가 보다 안정적이고 고품질의 애플리케이션을 만들 수 있도록 돕는 강력한 프레임워크입니다.
사용자 경험 중심 테스트의 중요성
과거의 기능 테스트가 특정 로직의 입력과 출력을 검증하는 데 집중했다면, 현대의 UI 테스트는 '사용자의 관점'에서 앱이 올바르게 동작하는지를 확인하는 데 초점을 맞춥니다. 사용자가 버튼을 클릭하고, 텍스트를 입력하고, 화면을 스크롤하는 일련의 과정을 코드로 시뮬레이션하여 실제 사용 환경에서 발생할 수 있는 오류를 사전에 발견하는 것이 목표입니다. Espresso는 바로 이러한 사용자 경험 중심의 테스트를 위해 설계되었습니다. 개발자는 Espresso를 통해 "사용자가 '로그인' 버튼을 누르면, '메인 화면'이 나타나야 한다"와 같은 시나리오를 매우 직관적인 코드로 작성할 수 있습니다.
Espresso의 핵심 철학: 자동 동기화
UI 테스트를 작성할 때 개발자가 마주하는 가장 큰 어려움 중 하나는 '동기화' 문제입니다. 예를 들어, 버튼을 클릭하면 네트워크 요청이 발생하고, 그 결과가 화면에 표시되는 시나리오를 생각해 봅시다. 테스트 코드는 네트워크 요청이 완료되고 UI가 갱신될 때까지 기다렸다가 검증을 수행해야 합니다. 그렇지 않으면 데이터가 표시되기도 전에 테스트가 실행되어 실패하게 됩니다. 이를 'Flaky test'(변덕스러운 테스트)라고 부르며, 테스트의 신뢰도를 크게 떨어뜨립니다. 많은 테스트 프레임워크는 이를 해결하기 위해 `Thread.sleep()`과 같은 명시적인 대기 코드를 삽입하도록 요구하지만, 이는 테스트 실행 시간을 불필요하게 늘리고 하드웨어 성능에 따라 결과가 달라지는 등 더 큰 문제를 야기합니다.
Espresso는 이러한 문제를 자동 동기화(Automatic Synchronization) 메커니즘으로 해결합니다. Espresso는 UI 이벤트 큐(UI event queue)와 AsyncTask의 백그라운드 스레드를 자동으로 감시합니다. 테스트 동작(Action)을 수행하기 전에 UI 스레드가 유휴(idle) 상태가 될 때까지 기다립니다. 즉, 화면에 애니메이션이 실행 중이거나 백그라운드 작업이 진행 중일 때는 다음 테스트 단계를 진행하지 않고 자동으로 대기합니다. 이 덕분에 개발자는 동기화 문제를 신경 쓰지 않고 테스트 로직 자체에만 집중할 수 있으며, 이는 Espresso가 제공하는 가장 강력하고 독보적인 장점입니다.
Espresso vs. 다른 테스트 프레임워크 (UI Automator, Appium)
안드로이드 테스트 생태계에는 Espresso 외에도 여러 프레임워크가 존재합니다. 각 프레임워크의 특징을 이해하고 상황에 맞는 도구를 선택하는 것이 중요합니다.
- Espresso:
- 종류: 화이트박스(White-box) 테스트 프레임워크
- 특징: 앱의 내부 코드와 리소스(R.id 등)에 직접 접근할 수 있습니다. 앱 프로세스 내에서 실행되므로 속도가 매우 빠르고 안정적입니다. 자동 동기화 기능이 강력합니다.
- 주요 용도: 단일 애플리케이션 내의 UI 흐름과 컴포넌트 상호작용을 테스트하는 데 최적화되어 있습니다.
- 한계: 테스트 대상 앱의 경계를 벗어날 수 없습니다. 즉, 다른 앱과의 상호작용이나 시스템 UI(알림 창, 설정 등)를 제어할 수 없습니다.
- UI Automator:
- 종류: 블랙박스(Black-box) 테스트 프레임워크
- 특징: 앱의 내부 구현을 알지 못한 채 사용자와 동일한 방식으로 UI를 제어합니다. 여러 앱 간의 상호작용 테스트가 가능하며, 시스템 UI도 제어할 수 있습니다.
- 주요 용도: 앱이 다른 앱(카메라, 갤러리 등)과 연동되는 시나리오나, 알림을 받고 반응하는 등의 통합 테스트에 적합합니다.
- 한계: Espresso보다 속도가 느리고, 동기화 처리가 까다로워 Flaky test가 발생할 확률이 높습니다.
- Appium:
- 종류: 블랙박스 테스트 프레임워크 (크로스 플랫폼)
- 특징: WebDriver 프로토콜을 사용하여 안드로이드와 iOS 앱을 모두 테스트할 수 있는 크로스 플랫폼 프레임워크입니다. 다양한 프로그래밍 언어(Java, Python, JavaScript 등)로 테스트 코드를 작성할 수 있습니다.
- 주요 용도: 동일한 테스트 로직으로 여러 플랫폼을 동시에 테스트해야 하는 경우에 유용합니다.
- 한계: 설정이 복잡하고, 네이티브 프레임워크(Espresso, XCUITest)에 비해 실행 속도가 현저히 느리며 안정성이 떨어집니다.
결론적으로, 특정 앱의 내부 기능과 UI 흐름을 안정적이고 빠르게 테스트하는 것이 목적이라면 Espresso가 가장 적합한 선택입니다.
Espresso 테스트의 기본 구성 요소: ViewMatcher, ViewAction, ViewAssertion
Espresso 테스트 코드는 대부분 다음 세 가지 구성 요소의 조합으로 이루어집니다. 이 구조는 "어떤 뷰를 찾아서(Matcher), 어떤 행동을 하고(Action), 어떤 상태인지 확인(Assertion)한다"는 자연스러운 흐름을 따릅니다.
onView(ViewMatcher).perform(ViewAction).check(ViewAssertion);
onView(ViewMatcher): 화면의 뷰 계층(View Hierarchy)에서 테스트하려는 특정 뷰를 찾는 역할을 합니다.withId(R.id.button),withText("Login")과 같이 뷰의 속성을 이용해 대상을 지정합니다..perform(ViewAction): 찾은 뷰에 대해 사용자의 행동을 시뮬레이션합니다.click(),typeText("hello"),scrollTo()등이 여기에 해당합니다..check(ViewAssertion): 행동이 수행된 후, 뷰의 상태가 기대하는 바와 일치하는지 검증합니다.matches(isDisplayed())(화면에 보이는지),matches(withText("Success"))(특정 텍스트를 포함하는지) 등이 있습니다.
이 세 가지 요소의 조합을 통해 매우 직관적이고 가독성 높은 테스트 코드를 작성할 수 있습니다. 예를 들어 "ID가 `email_input`인 EditText에 'test@example.com'을 입력한다"는 동작은 다음과 같이 표현할 수 있습니다.
onView(withId(R.id.email_input)).perform(typeText("test@example.com"));
이러한 간결함과 명확성은 Espresso가 개발자들에게 사랑받는 또 다른 이유입니다.
2. Espresso를 위한 완벽한 개발 환경 설정
Espresso의 강력한 기능을 활용하기 위해서는 먼저 프로젝트에 올바르게 통합하는 과정이 필요합니다. 단순히 의존성을 추가하는 것을 넘어, 안정적인 테스트 환경을 구축하기 위한 몇 가지 추가적인 설정이 권장됩니다. 이 장에서는 기본적인 의존성 설정부터 테스트 안정성을 극대화하기 위한 팁까지 상세히 다룹니다.
필수 의존성 추가: Core, Contrib, Intents, Web
Espresso는 모듈화된 라이브러리 모음입니다. 프로젝트의 필요에 따라 적절한 의존성을 추가해야 합니다. 일반적으로 모듈 수준의 `build.gradle` 파일 (app/build.gradle)의 `dependencies` 블록에 다음 의존성들을 추가합니다.
dependencies {
// ... 다른 의존성들
// Espresso Core: 기본적인 View 상호작용을 위한 필수 라이브러리
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
// JUnit Jupiter: 최신 JUnit 5 기반 테스트 작성을 위함
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
// Test Rules: ActivityScenario 등 테스트 규칙을 제공
androidTestImplementation 'androidx.test:rules:1.5.0'
// Espresso Contrib: RecyclerView, DrawerLayout 등 복잡한 UI 컴포넌트 테스트 지원
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
// Espresso Intents: Intent 발생 여부 및 내용을 검증하는 기능
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
// Espresso Web: WebView 내부의 웹 콘텐츠와 상호작용하는 기능
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
}
espresso-core:onView(),perform(),check()등 Espresso의 핵심 API를 포함하는 필수 라이브러리입니다.espresso-contrib:RecyclerView의 특정 위치로 스크롤하거나 아이템을 클릭하는 등 복잡한 UI 위젯을 쉽게 테스트할 수 있는 유용한 도우미 클래스들을 제공합니다.DatePicker,DrawerActions등도 포함됩니다.espresso-intents: '공유하기' 버튼을 눌렀을 때 올바른 `ACTION_SEND` 인텐트가 발생하는지, 또는 특정 액티비티가 정확한 엑스트라 데이터와 함께 시작되는지를 검증할 수 있게 해주는 강력한 확장 라이브러리입니다.espresso-web: 앱 내에 포함된WebView의 DOM 요소와 상호작용(클릭, 텍스트 입력 등)하고 검증할 수 있는 기능을 제공합니다.
build.gradle 설정 심층 분석
의존성 추가 외에도, `build.gradle` 파일에서 테스트 실행기와 관련된 몇 가지 중요한 설정을 확인하고 구성해야 합니다.
android {
// ...
defaultConfig {
// ...
// AndroidJUnitRunner를 테스트 실행기로 지정합니다.
// 이는 안드로이드 환경에서 JUnit 테스트를 실행하기 위한 표준 실행기입니다.
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
// packagingOptions는 라이브러리 간 충돌을 방지하기 위해 필요할 수 있습니다.
// 예를 들어, 여러 라이브러리가 동일한 파일을 포함할 때 빌드 오류가 발생할 수 있습니다.
packagingOptions {
resources.excludes.add("META-INF/LICENSE*")
}
}
testInstrumentationRunner를 AndroidJUnitRunner로 설정하는 것은 AndroidX Test 라이브러리를 사용하여 기기나 에뮬레이터에서 테스트를 실행하겠다는 의미입니다. 대부분의 안드로이드 스튜디오 프로젝트에 기본으로 설정되어 있지만, 올바르게 지정되어 있는지 확인하는 것이 중요합니다.
테스트 안정성을 위한 기기 및 에뮬레이터 설정 (애니메이션 비활성화)
Espresso의 자동 동기화 기능은 매우 강력하지만, 시스템 레벨의 애니메이션은 때때로 테스트의 안정성을 해치는 요인이 될 수 있습니다. 예를 들어, 액티비티 전환 애니메이션이나 창 애니메이션이 완료되기 전에 Espresso가 다음 동작을 시도하면 테스트가 실패할 수 있습니다. 따라서 UI 테스트를 실행하는 기기나 에뮬레이터에서는 모든 종류의 애니메이션을 비활성화하는 것이 강력하게 권장됩니다.
이 설정은 '개발자 옵션' 메뉴에서 수동으로 할 수 있지만, CI/CD 환경이나 여러 개발자가 협업하는 환경에서는 ADB 명령어를 통해 자동화하는 것이 효율적입니다.
터미널이나 명령 프롬프트에서 다음 세 가지 명령어를 실행하세요:
# 창 애니메이션 배율 비활성화
adb shell settings put global window_animation_scale 0
# 전환 애니메이션 배율 비활성화
adb shell settings put global transition_animation_scale 0
# Animator 길이 배율 비활성화
adb shell settings put global animator_duration_scale 0
이 명령어들은 테스트 실행 전에 스크립트로 실행하거나, Gradle 태스크로 만들어 관리하면 테스트 환경을 일관되게 유지하는 데 큰 도움이 됩니다. 애니메이션을 비활성화하면 UI 상태 전환이 즉시 이루어지므로 Espresso의 동기화 메커니즘이 더 빠르고 안정적으로 작동하여 'Flaky test'를 획기적으로 줄일 수 있습니다.
테스트 실행기(Test Runner) 설정 및 이해
AndroidJUnitRunner는 단순한 실행기 이상의 역할을 합니다. 테스트 환경을 설정하고, 테스트 클래스와 메소드를 찾아 실행하며, 결과를 보고하는 모든 과정을 담당합니다. build.gradle 파일에서 `testInstrumentationRunnerArguments`를 통해 이 실행기의 동작을 미세 조정할 수 있습니다.
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// testInstrumentationRunnerArguments를 통해 추가 옵션 전달
testInstrumentationRunnerArguments([
'clearPackageData': 'true', // 각 테스트 실행 전에 앱 데이터 초기화
'disableAnalytics': 'true', // Firebase Analytics 등 분석 도구 비활성화
'class': 'com.example.MyTestSuite' // 특정 테스트 클래스나 스위트만 실행
])
}
}
특히 clearPackageData 옵션을 true로 설정하면, 매 테스트 실행마다 앱의 데이터(SharedPreferences, 데이터베이스 등)가 초기화된 상태에서 시작되므로 테스트 간의 의존성을 제거하고 독립성을 보장하는 데 매우 유용합니다. 이러한 설정들을 통해 더 제어되고 예측 가능한 테스트 환경을 구축할 수 있습니다.
3. 핵심부터 고급까지: Espresso 테스트 케이스 작성
환경 설정이 완료되었다면 이제 본격적으로 테스트 코드를 작성할 차례입니다. Espresso 테스트는 배우기 쉽지만, 그 깊이는 상당합니다. 이 장에서는 기본적인 테스트 구조부터 시작하여 다양한 UI 컴포넌트를 다루는 고급 기법까지 체계적으로 학습합니다.
테스트 클래스의 구조: @RunWith, @Rule, @Before, @After, @Test
Espresso 테스트 클래스는 일반적으로 JUnit4(또는 JUnit5)의 어노테이션을 기반으로 구성됩니다. 각 어노테이션의 역할을 정확히 이해하는 것이 중요합니다.
// 1. @RunWith: 이 클래스를 AndroidJUnit4 테스트 실행기로 실행하도록 지정
@RunWith(AndroidJUnit4.class)
public class LoginActivityTest {
// 2. @Rule: 각 테스트 메소드 실행 전후에 특정 동작을 수행하는 규칙을 정의
// ActivityScenarioRule은 테스트할 액티비티를 시작하고, 테스트가 끝나면 종료합니다.
@Rule
public ActivityScenarioRule<LoginActivity> activityRule =
new ActivityScenarioRule<>(LoginActivity.class);
// 3. @Before: 각 테스트 메소드(@Test)가 실행되기 *직전에* 호출되는 메소드
@Before
public void setUp() {
// 테스트에 필요한 사전 설정 (예: Mock 객체 초기화, 데이터베이스 클리어)
}
// 4. @Test: 실제 테스트 로직을 포함하는 메소드
@Test
public void loginSuccess_withValidCredentials() {
// 1. Arrange (준비)
String email = "test@example.com";
String password = "password123";
// 2. Act (실행)
onView(withId(R.id.editText_email)).perform(typeText(email), closeSoftKeyboard());
onView(withId(R.id.editText_password)).perform(typeText(password), closeSoftKeyboard());
onView(withId(R.id.button_login)).perform(click());
// 3. Assert (검증)
onView(withId(R.id.textView_welcome)).check(matches(withText("Welcome!")));
}
@Test
public void loginFail_withInvalidPassword() {
// ... 또 다른 테스트 케이스
}
// 5. @After: 각 테스트 메소드가 실행된 *직후에* 호출되는 메소드
@After
public void tearDown() {
// 사용한 리소스 정리 (예: Idling Resource 등록 해제, Mock 서버 종료)
}
}
@RunWith(AndroidJUnit4.class): 이 클래스가 안드로이드 테스트 환경에서 실행되어야 함을 JUnit에 알립니다.@Rule: JUnit의 강력한 기능으로, 테스트 메소드를 감싸는 외부 로직을 정의합니다.ActivityScenarioRule은 가장 흔하게 사용되는 규칙으로, 테스트 시작 시 지정된 액티비티를 안전하게 실행하고 테스트 종료 시 정리해주는 역할을 합니다. 과거에는ActivityTestRule이 사용되었지만, 이제는 수명 주기를 더 세밀하게 제어할 수 있는ActivityScenarioRule사용이 권장됩니다.@Before/@After: 여러 테스트에서 공통적으로 필요한 준비(setup) 및 정리(teardown) 작업을 처리하는 데 사용됩니다. 이를 통해 테스트 코드의 중복을 줄이고 각 테스트가 독립적인 환경에서 실행되도록 보장할 수 있습니다.@Test: 개별 테스트 케이스를 정의합니다. 하나의@Test메소드는 하나의 특정 시나리오나 기능을 검증하는 데 집중해야 합니다.
ViewMatcher 정복: 원하는 뷰를 정확하게 찾는 기술
테스트의 첫걸음은 상호작용할 뷰를 정확하게 찾는 것입니다. Espresso는 ViewMatchers 클래스를 통해 매우 다양하고 강력한 Matcher를 제공합니다.
- 기본 Matcher:
withId(R.id.some_id): 가장 일반적이고 안정적인 방법입니다. XML 레이아웃에 정의된 ID를 사용합니다.withText("Some Text"): 특정 텍스트를 가진 뷰(Button, TextView 등)를 찾습니다.withHint("Enter username"): 특정 힌트 텍스트를 가진 EditText를 찾습니다.withContentDescription("Navigate up"): 콘텐츠 설명(접근성을 위해 사용)을 가진 뷰(주로 ImageButton)를 찾습니다.
- 상태 Matcher:
isDisplayed(): 화면에 실제로 보이는 뷰를 찾습니다.isEnabled()/isNotEnabled(): 활성화/비활성화된 뷰를 찾습니다.isChecked()/isNotChecked(): 체크된/체크되지 않은 뷰(CheckBox, RadioButton)를 찾습니다.
- 조합 Matcher (Hamcrest):
allOf(...): 여러 Matcher 조건을 모두 만족하는 뷰를 찾습니다. 예를 들어, `allOf(withId(R.id.button), withText("Submit"), isEnabled())`는 ID가 button이고, 텍스트가 "Submit"이며, 활성화된 상태인 뷰를 찾습니다.anyOf(...): 여러 Matcher 조건 중 하나라도 만족하는 뷰를 찾습니다.not(...): 특정 Matcher 조건을 만족하지 않는 뷰를 찾습니다.
팁: ID로 뷰를 찾는 것이 가장 안정적입니다. 텍스트는 다국어 지원 등으로 인해 변경될 수 있기 때문입니다. 하지만 동적으로 생성되는 뷰나 `RecyclerView`의 아이템처럼 ID를 사용하기 어려운 경우에는 다른 Matcher들을 조합하여 사용해야 합니다.
만약 동일한 조건을 만족하는 뷰가 여러 개 있으면 Espresso는 `AmbiguousViewMatcherException`을 발생시킵니다. 이 경우, `allOf`를 사용해 조건을 더 구체적으로 명시하여 유일한 뷰를 찾도록 해야 합니다.
ViewAction 활용: 사용자 상호작용 시뮬레이션
뷰를 찾았다면, 이제 사용자가 할 법한 행동을 시뮬레이션할 차례입니다. ViewActions 클래스는 다양한 사용자 행동을 제공합니다.
click(): 뷰를 클릭합니다.doubleClick(): 뷰를 더블 클릭합니다.longClick(): 뷰를 길게 클릭합니다.typeText("hello"): EditText에 텍스트를 입력합니다. 이 액션은 키보드를 자동으로 열지 않을 수 있습니다.typeTextIntoFocusedView("hello"): 현재 포커스된 뷰에 텍스트를 입력합니다.replaceText("new text"): EditText의 기존 텍스트를 지우고 새로운 텍스트로 대체합니다.clearText(): EditText의 텍스트를 모두 지웁니다.pressKey(KeyEvent.KEYCODE_ENTER): 특정 키 입력을 시뮬레이션합니다.closeSoftKeyboard(): 화면에 나타난 소프트 키보드를 닫습니다. 텍스트 입력 후 다른 뷰를 클릭하기 전에 호출하는 것이 좋습니다.scrollTo():ScrollView나NestedScrollView내에서 특정 뷰가 화면에 보이도록 스크롤합니다. 이 액션은 뷰가ScrollView의 자식일 때만 동작합니다.swipeLeft(),swipeRight(),swipeUp(),swipeDown(): 뷰를 스와이프합니다.ViewPager등에서 사용됩니다.
여러 액션을 쉼표(,)로 구분하여 perform() 메소드에 전달하면 순차적으로 실행됩니다. 예를 들어, `perform(typeText("test"), closeSoftKeyboard())`와 같이 사용할 수 있습니다.
ViewAssertion으로 검증: UI 상태 단언하기
테스트의 마지막 단계는 액션의 결과로 UI가 기대하는 상태로 변경되었는지 확인하는 것입니다. `ViewAssertions` 클래스는 `check()` 메소드와 함께 사용되는 두 가지 주요 Assertion을 제공합니다.
matches(Matcher<View>): 뷰가 특정 상태를 만족하는지 검증합니다.ViewMatcher에서 사용했던 대부분의 Matcher를 여기에서도 사용할 수 있습니다.matches(isDisplayed()): 뷰가 화면에 보이는지 확인합니다.matches(not(isDisplayed())): 뷰가 화면에 보이지 않는지 확인합니다.matches(withText("Success!")): 뷰의 텍스트가 "Success!"인지 확인합니다.matches(isEnabled()): 뷰가 활성화 상태인지 확인합니다.matches(hasErrorText("Invalid input")): EditText에 특정 에러 텍스트가 표시되는지 확인합니다.
doesNotExist(): 뷰가 뷰 계층에 존재하지 않음을 단언합니다. 예를 들어, 항목을 삭제한 후 해당 항목이 더 이상 존재하지 않는지 확인할 때 사용합니다.
만약 뷰를 찾을 수 없는데 `matches()`를 사용하면 `NoMatchingViewException`이 발생하고, 반대로 뷰가 존재하는데 `doesNotExist()`를 사용하면 테스트가 실패합니다. 이를 통해 UI의 존재 유무를 명확하게 검증할 수 있습니다.
복잡한 UI 다루기: RecyclerView, AdapterView, Dialog, Toast
기본적인 뷰 외에 안드로이드 앱에는 더 복잡한 UI 컴포넌트들이 많습니다. Espresso는 이러한 컴포넌트들을 테스트하기 위한 별도의 방법을 제공합니다.
RecyclerView
RecyclerView는 동적인 리스트를 표시하는 데 널리 사용됩니다. 리스트의 특정 아이템과 상호작용하기 위해서는 `espresso-contrib` 라이브러리의 RecyclerViewActions를 사용해야 합니다.
// 10번째 포지션으로 스크롤
onView(withId(R.id.recycler_view)).perform(scrollToPosition(10));
// "Hello World" 텍스트를 가진 아이템을 찾아 그 아이템의 'item_button'을 클릭
onView(withId(R.id.recycler_view))
.perform(actionOnItem(
hasDescendant(withText("Hello World")),
clickDescendant(R.id.item_button)
));
// 특정 포지션(5번)에 있는 아이템의 뷰 전체를 클릭
onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition(5, click()));
// 커스텀 ViewAction을 정의하여 자식 뷰 클릭 구현
public static ViewAction clickDescendant(final int id) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return any(View.class);
}
@Override
public String getDescription() {
return "Click on a descendant view with id " + id;
}
@Override
public void perform(UiController uiController, View view) {
View v = view.findViewById(id);
if (v != null) {
v.performClick();
}
}
};
}
AdapterView (ListView, Spinner)
ListView나 Spinner와 같은 `AdapterView`는 onView() 대신 onData()를 사용하여 아이템과 상호작용합니다. onData()는 뷰가 아닌 어댑터의 데이터 자체를 기반으로 아이템을 찾습니다.
// 어댑터 데이터 중 "Item 5"라는 문자열을 가진 아이템을 클릭
onData(allOf(is(instanceOf(String.class)), is("Item 5"))).perform(click());
// Spinner에서 "Espresso"라는 텍스트를 가진 아이템을 선택
onView(withId(R.id.spinner)).perform(click());
onData(allOf(is(instanceOf(String.class)), is("Espresso"))).perform(click());
// 선택된 아이템이 "Espresso"인지 확인
onView(withId(R.id.spinner)).check(matches(withSpinnerText(containsString("Espresso"))));
Dialog와 Toast
다이얼로그나 토스트 메시지는 일반적인 액티비티의 뷰 계층과 다른 윈도우에 속해있습니다. 따라서 이들을 테스트하려면 루트 뷰를 변경해야 합니다.
// 다이얼로그의 텍스트 확인
onView(withText("Are you sure?"))
.inRoot(isDialog()) // 검색 범위를 다이얼로그로 한정
.check(matches(isDisplayed()));
// 다이얼로그의 'OK' 버튼 클릭
onView(withText("OK"))
.inRoot(isDialog())
.perform(click());
// 토스트 메시지 확인 (가장 까다로운 케이스 중 하나)
// 토스트는 매우 짧은 시간 동안만 나타나므로 동기화 문제가 발생하기 쉽습니다.
// decorView를 기반으로 루트를 찾아야 합니다.
@Rule
public ActivityScenarioRule<MainActivity> activityRule = new ActivityScenarioRule<>(MainActivity.class);
@Test
public void testToastMessage() {
// 토스트를 발생시키는 동작 수행
onView(withId(R.id.button_show_toast)).perform(click());
// decorView를 가져와서 토스트 메시지가 해당 윈도우에 있는지 확인
activityRule.getScenario().onActivity(activity -> {
onView(withText(R.string.toast_message))
.inRoot(withDecorView(not(is(activity.getWindow().getDecorView()))))
.check(matches(isDisplayed()));
});
}
화면 전환 테스트: Espresso-Intents 활용법
espresso-intents 라이브러리를 사용하면 액티비티 간의 이동(Intent)을 테스트할 수 있습니다. 이는 실제 액티비티를 실행하지 않고도 인텐트가 올바르게 생성되고 전송되는지만을 검증(stubbing)하거나, 실제로 발생한 인텐트를 검증(verification)하는 두 가지 방식으로 활용됩니다.
먼저 `IntentsTestRule` (또는 `Intents.init()`/`release()`)을 사용하여 인텐트 감지를 활성화해야 합니다.
@RunWith(AndroidJUnit4.class)
public class ContactPickerTest {
// IntentsTestRule을 사용하면 @Test 전후로 자동으로 init/release가 호출됩니다.
@Rule
public IntentsTestRule<MainActivity> intentsRule =
new IntentsTestRule<>(MainActivity.class);
@Test
public void testValidateEmailIntent() {
// 1. Stubbing: ACTION_PICK 인텐트가 발생하면 가짜 결과(Uri)를 반환하도록 설정
Intent resultData = new Intent();
Uri fakeUri = Uri.parse("content://contacts/people/1");
resultData.setData(fakeUri);
Instrumentation.ActivityResult result =
new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
intending(hasAction(Intent.ACTION_PICK)).respondWith(result);
// 2. Act: 주소록을 여는 버튼 클릭
onView(withId(R.id.button_pick_contact)).perform(click());
// 3. Verification: 실제로 ACTION_PICK 인텐트가 발생했는지 검증
intended(hasAction(Intent.ACTION_PICK));
// 4. Assert: 반환된 가짜 Uri가 화면에 올바르게 표시되는지 확인
onView(withId(R.id.textView_contact_uri)).check(matches(withText(fakeUri.toString())));
}
}
위 예제에서는 intending을 사용해 외부 액티비티(주소록)를 실제로 실행하는 대신 미리 정의된 결과를 반환하도록 설정했습니다. 이를 통해 외부 앱의 상태에 의존하지 않는 안정적인 테스트를 만들 수 있습니다. 그리고 intended를 사용해 우리가 의도한 인텐트가 정확히 발생했는지 사후에 검증합니다.
웹뷰(WebView) 콘텐츠 테스트: Espresso-Web
espresso-web은 WebView 내부의 HTML 요소와 상호작용할 수 있게 해줍니다. 이는 WebDriver API와 유사한 방식으로 동작합니다.
// 먼저 WebView와 상호작용하도록 설정
onWebView().forceJavascriptEnabled();
// ID가 'name_input'인 HTML input 요소에 텍스트 입력
onWebView(withId(R.id.web_view))
.withElement(findElement(Locator.ID, "name_input"))
.perform(webKeys("John Doe"));
// 'Submit' 버튼 클릭
onWebView(withId(R.id.web_view))
.withElement(findElement(Locator.ID, "submit_button"))
.perform(webClick());
// 결과 텍스트가 'Hello, John Doe!'인지 확인
onWebView(withId(R.id.web_view))
.withElement(findElement(Locator.ID, "result_text"))
.check(webMatches(getText(), containsString("Hello, John Doe!")));
espresso-web을 사용하면 네이티브 앱과 웹 콘텐츠가 결합된 하이브리드 앱의 E2E(End-to-End) 테스트 시나리오를 효과적으로 검증할 수 있습니다.
4. 테스트 실행, 분석 및 디버깅
정성껏 작성한 테스트 코드는 실행하고 그 결과를 분석해야 비로소 가치를 가집니다. 테스트가 실패했을 때 원인을 빠르고 정확하게 파악하는 능력 또한 중요합니다. 이 장에서는 테스트를 실행하는 다양한 방법과 결과 보고서를 해석하고, 실패한 테스트를 디버깅하는 실용적인 기술들을 다룹니다.
다양한 테스트 실행 방법: Android Studio vs. Gradle CLI
Espresso 테스트는 크게 두 가지 방법으로 실행할 수 있습니다.
Android Studio에서 실행
개발 과정에서 가장 흔하게 사용하는 방법입니다.
- 개별 테스트 메소드 실행: 테스트 메소드 이름 옆에 있는 녹색 '실행' 아이콘(▶)을 클릭하면 해당 테스트 케이스만 실행됩니다. 특정 시나리오를 디버깅하거나 수정할 때 유용합니다.
- 테스트 클래스 전체 실행: 테스트 클래스 이름 옆의 '실행' 아이콘을 클릭하면 해당 클래스에 포함된 모든
@Test메소드가 실행됩니다. - 특정 패키지/모듈 전체 실행:
androidTest폴더를 우클릭하고 'Run tests in ...'를 선택하면 해당 범위의 모든 테스트가 실행됩니다.
Android Studio는 실행 결과를 IDE 하단의 'Run' 탭에 시각적으로 보여주어 성공/실패 여부를 즉시 확인할 수 있다는 장점이 있습니다.
Gradle Command Line Interface (CLI)에서 실행
터미널에서 Gradle 명령어를 사용하여 테스트를 실행하는 방법은 CI/CD(지속적 통합/지속적 배포) 파이프라인에 통합하는 데 필수적입니다.
# 연결된 모든 기기/에뮬레이터에서 androidTest 실행
./gradlew connectedAndroidTest
# 특정 빌드 변형(variant)에 대해서만 테스트 실행 (예: debug)
./gradlew connectedDebugAndroidTest
# 특정 테스트 클래스만 실행
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.LoginActivityTest
# 특정 테스트 메소드만 실행
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.LoginActivityTest#loginSuccess_withValidCredentials
CLI를 사용하면 빌드 서버(Jenkins, GitLab CI, GitHub Actions 등)에서 자동으로 테스트를 실행하고 결과를 리포팅하는 과정을 자동화할 수 있습니다. 이는 코드 변경 사항이 기존 기능에 영향을 미치지 않았는지(회귀 테스트) 지속적으로 검증하는 데 매우 중요합니다.
테스트 결과 보고서 완벽 해독하기
Gradle을 통해 테스트를 실행하면 상세한 HTML 보고서가 생성됩니다. 이 보고서는 app/build/reports/androidTests/connected/ 디렉토리에 위치합니다. index.html 파일을 웹 브라우저로 열면 모든 테스트의 결과를 한눈에 볼 수 있습니다.
보고서에는 다음과 같은 정보가 포함됩니다:
- 요약: 전체 테스트 수, 성공/실패 수, 실행 시간 등 종합적인 통계.
- 패키지/클래스별 결과: 테스트 결과를 계층적으로 보여주어 어떤 부분에서 실패가 많이 발생하는지 파악하기 용이합니다.
- 실패한 테스트 상세 정보: 실패한 테스트를 클릭하면 어떤 기기에서, 어떤 이유로 실패했는지에 대한 상세한 스택 트레이스(stack trace)를 확인할 수 있습니다.
- 표준 출력/오류: 테스트 실행 중 발생한 로그(Logcat)나 표준 출력 메시지도 함께 볼 수 있어 디버깅에 큰 도움이 됩니다.
이 HTML 보고서는 테스트 실행 결과를 공유하고 분석하는 표준적인 방법이므로, 그 구조를 잘 이해하고 활용하는 것이 좋습니다.
실패는 성공의 어머니: 일반적인 Espresso 예외와 디버깅 전략
Espresso 테스트가 실패하면 특정 예외(Exception)가 발생합니다. 이 예외의 의미를 이해하면 문제 해결의 실마리를 찾을 수 있습니다.
NoMatchingViewException:- 원인: 지정한 Matcher 조건에 맞는 뷰를 뷰 계층에서 찾지 못했을 때 발생합니다.
- 해결 전략:
- 뷰의 ID나 텍스트가 올바른지 확인합니다.
- 뷰가 화면에 나타나기 전에 너무 빨리 찾으려고 한 것은 아닌지 확인합니다 (비동기 작업 문제).
- 뷰가 다른 뷰에 가려져 있거나, `visibility`가 `GONE`으로 설정된 것은 아닌지 확인합니다.
AmbiguousViewMatcherException:- 원인: 지정한 Matcher 조건에 맞는 뷰가 두 개 이상일 때 발생합니다. Espresso는 어떤 뷰와 상호작용해야 할지 모르기 때문에 예외를 던집니다.
- 해결 전략:
allOf를 사용하여 조건을 더 구체적으로 명시해야 합니다. 예를 들어,allOf(withText("Button"), withId(R.id.specific_button))와 같이 부모 뷰의 ID나 다른 속성을 조합하여 뷰를 유일하게 특정합니다.
PerformException:- 원인: 뷰는 찾았지만, 해당 뷰에 특정 액션을 수행할 수 없을 때 발생합니다. 예를 들어, 화면에 보이지 않는(`isDisplayed()`가 false인) 뷰에
click()을 시도하거나,ScrollView내부에 있지 않은 뷰에scrollTo()를 시도하는 경우입니다. - 해결 전략: 액션을 수행하기 전에 뷰가 적절한 상태(예: 화면에 보이고, 클릭 가능한 위치에 있는지)인지 확인합니다. 필요하다면
scrollTo()를 먼저 수행하여 뷰를 화면에 보이게 만들어야 합니다.
- 원인: 뷰는 찾았지만, 해당 뷰에 특정 액션을 수행할 수 없을 때 발생합니다. 예를 들어, 화면에 보이지 않는(`isDisplayed()`가 false인) 뷰에
AssertionFailedError:- 원인:
check()를 통해 검증한 내용이 사실이 아닐 때 발생합니다. 예를 들어,matches(withText("Success"))로 검증했는데 실제 텍스트는 "Failure"인 경우입니다. - 해결 전략: 테스트 로직 자체의 문제이거나, 실제 앱의 동작이 기대와 다른 경우입니다. 앱의 비즈니스 로직과 UI 상태 변화를 다시 한번 점검해야 합니다.
- 원인:
디버깅 팁: Android Studio의 디버거는 UI 테스트에서도 동일하게 작동합니다. 테스트 코드에 중단점(breakpoint)을 설정하고 '디버그' 모드로 테스트를 실행하면, 특정 시점의 뷰 계층 구조나 변수의 상태를 자세히 살펴볼 수 있어 복잡한 문제의 원인을 파악하는 데 매우 유용합니다.
테스트 커버리지 측정 및 개선 (JaCoCo)
테스트 커버리지는 작성된 테스트 코드가 실제 애플리케이션 코드를 얼마나 실행했는지를 나타내는 지표입니다. 100% 커버리지가 항상 좋은 것은 아니지만, 테스트가 부족한 부분을 식별하고 테스트의 품질을 정량적으로 평가하는 데 도움이 됩니다.
JaCoCo(Java Code Coverage)는 안드로이드에서 널리 사용되는 코드 커버리지 측정 도구입니다. `build.gradle` 파일에 다음과 같이 설정을 추가하면 커버리지 리포트를 생성할 수 있습니다.
android {
// ...
buildTypes {
debug {
// debug 빌드에서 테스트 커버리지를 활성화합니다.
testCoverageEnabled true
}
}
}
설정 후, 다음 Gradle 명령어를 실행하면 테스트가 실행되고 커버리지 결과가 생성됩니다.
./gradlew createDebugCoverageReport
리포트는 app/build/reports/coverage/debug/ 디렉토리에 생성되며, index.html 파일을 통해 어떤 클래스와 메소드가 테스트되었고, 어떤 부분이 테스트되지 않았는지 시각적으로 확인할 수 있습니다. 이를 바탕으로 테스트가 부족한 로직(특히 분기문, 예외 처리 등)에 대한 테스트 케이스를 보강하는 전략을 세울 수 있습니다.
5. 유지보수 가능한 테스트를 위한 고급 전략과 모범 사례
테스트 코드는 애플리케이션 코드만큼이나 중요하며, 지속적으로 유지보수되어야 합니다. 앱의 기능이 복잡해질수록 테스트 코드도 함께 복잡해지기 마련입니다. 이 장에서는 테스트 코드의 안정성, 가독성, 재사용성을 높여 장기적으로 유지보수하기 좋은 테스트를 작성하기 위한 고급 전략과 모범 사례를 소개합니다.
비동기 처리와의 전쟁: Idling Resource 심층 탐구
Espresso의 자동 동기화는 AsyncTask와 UI 스레드 메시지 큐에 대해서는 잘 작동하지만, 개발자가 직접 생성한 백그라운드 스레드, RxJava/Coroutine을 사용한 비동기 작업, OkHttp를 통한 네트워크 요청 등은 감지하지 못합니다. 이런 경우, Espresso는 비동기 작업이 끝나기를 기다리지 않고 바로 다음 코드를 실행하여 테스트가 실패하게 됩니다.
IdlingResource는 바로 이 문제를 해결하기 위해 존재합니다. 이는 앱의 비동기 작업이 '진행 중'인지 '유휴 상태'인지를 Espresso에 알려주는 간단한 인터페이스입니다. Espresso는 등록된 모든 IdlingResource가 '유휴 상태'가 될 때까지 테스트 실행을 자동으로 멈춥니다.
커스텀 Idling Resource 구현 예제
예를 들어, 간단한 카운터가 백그라운드에서 작업하는 시나리오를 생각해 봅시다.
// 1. CountingIdlingResource 구현 (가장 간단하고 흔한 방법)
// build.gradle에 espresso-idling-resource 의존성 추가 필요
// androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
public class MyIdlingResource {
private static final String RESOURCE = "GLOBAL";
// CountingIdlingResource는 내부적으로 카운터를 가지고 있음
public static CountingIdlingResource mIdlingResource = new CountingIdlingResource(RESOURCE);
public static void increment() {
mIdlingResource.increment(); // 작업 시작 시 카운터 증가
}
public static void decrement() {
if (!mIdlingResource.isIdleNow()) {
mIdlingResource.decrement(); // 작업 종료 시 카운터 감소
}
}
}
// 2. 실제 애플리케이션 코드에서 사용
public void performBackgroundTask() {
MyIdlingResource.increment(); // 작업 시작 알림
new Thread(() -> {
try {
Thread.sleep(3000); // 3초간 작업 시뮬레이션
} catch (InterruptedException e) {
e.printStackTrace();
}
// 작업이 끝나면 UI 스레드에서 UI 업데이트
runOnUiThread(() -> {
textView.setText("Task Completed");
MyIdlingResource.decrement(); // 작업 종료 알림
});
}).start();
}
// 3. 테스트 코드에서 등록 및 사용
@RunWith(AndroidJUnit4.class)
public class IdlingResourceTest {
@Rule
public ActivityScenarioRule<MainActivity> activityRule = new ActivityScenarioRule<>(MainActivity.class);
@Before
public void registerIdlingResource() {
// 테스트 시작 전에 IdlingResource를 Espresso에 등록
IdlingRegistry.getInstance().register(MyIdlingResource.mIdlingResource);
}
@Test
public void testBackgroundTask() {
// 버튼을 클릭하면 3초짜리 백그라운드 작업 시작
onView(withId(R.id.button_start_task)).perform(click());
// Espresso는 MyIdlingResource가 idle 상태(카운터가 0)가 될 때까지 자동으로 기다림
// 따라서 Thread.sleep()이 필요 없음!
// 3초 후, 작업이 완료되면 텍스트가 변경되었는지 확인
onView(withId(R.id.textView_status)).check(matches(withText("Task Completed")));
}
@After
public void unregisterIdlingResource() {
// 테스트 종료 후 반드시 등록 해제하여 메모리 누수 방지
IdlingRegistry.getInstance().unregister(MyIdlingResource.mIdlingResource);
}
}
이처럼 IdlingResource를 사용하면 Thread.sleep()과 같은 불안정한 코드를 완전히 제거하고, 비동기 작업이 실제로 끝나는 시점에 정확히 맞춰 검증을 수행할 수 있어 테스트의 신뢰성을 극대화할 수 있습니다. OkHttp, RxJava 등 유명 라이브러리들은 이미 Idling Resource를 지원하는 별도의 라이브러리를 제공하므로, 직접 구현하기 전에 찾아보는 것이 좋습니다.
테스트의 독립성과 격리: Hermetic 테스트와 Test Orchestrator
Hermetic 테스트란 외부 환경(네트워크, 데이터베이스, 다른 앱 등)에 의존하지 않고 완전히 격리된 환경에서 실행되는 테스트를 의미합니다. 이는 테스트의 안정성과 속도를 크게 향상시킵니다.
- 의존성 주입(Dependency Injection): Hilt나 Dagger 같은 DI 프레임워크를 사용하여 테스트 시에는 실제 네트워크 모듈이나 데이터베이스 대신 가짜 데이터를 반환하는 Mock 객체를 주입합니다. 이를 통해 네트워크 오류나 서버 상태와 무관하게 UI 로직만을 테스트할 수 있습니다.
- Mock 서버 활용: MockWebServer와 같은 라이브러리를 사용하면, 실제 API 서버와 동일한 응답을 보내는 로컬 서버를 띄울 수 있습니다. 이를 통해 다양한 API 응답(성공, 실패, 에러 등)에 따른 UI 변화를 안정적으로 테스트할 수 있습니다.
Android Test Orchestrator는 각 UI 테스트를 자체 `Instrumentation` 샌드박스에서 실행하여 테스트 간의 격리를 강화하는 도구입니다. 일반적인 테스트 실행에서는 한 테스트가 비정상적으로 종료되면(crash) 이후의 모든 테스트가 실행되지 않고 중단됩니다. 또한, 테스트 간에 공유되는 상태(static 변수, SharedPreferences 등)가 다음 테스트에 영향을 미칠 수 있습니다. Test Orchestrator를 사용하면 각 테스트 실행 후 앱 프로세스가 완전히 종료되고 재시작되므로 이러한 문제를 원천적으로 방지할 수 있습니다.
build.gradle에 다음과 같이 설정하여 활성화할 수 있습니다.
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}
dependencies {
androidTestUtil 'androidx.test:orchestrator:1.4.2'
}
Orchestrator를 사용하면 테스트 실행 시간은 다소 늘어나지만, 대규모 테스트 스위트의 안정성을 확보하는 데 매우 효과적입니다.
페이지 객체 모델(Page Object Model) 패턴 적용하기
페이지 객체 모델(POM)은 UI 테스트 코드의 재사용성과 유지보수성을 높이기 위한 디자인 패턴입니다. 이 패턴의 핵심은 애플리케이션의 각 화면(페이지)을 하나의 클래스로 추상화하는 것입니다. 이 클래스는 해당 화면에 있는 UI 요소(뷰)와 그와 관련된 동작(메소드)들을 캡슐화합니다.
POM 적용 예제 (로그인 화면)
// LoginPage.java - 로그인 화면을 나타내는 페이지 객체
public class LoginPage {
// 1. UI 요소 정의 (Matcher)
private final Matcher<View> emailField = withId(R.id.editText_email);
private final Matcher<View> passwordField = withId(R.id.editText_password);
private final Matcher<View> loginButton = withId(R.id.button_login);
private final Matcher<View> errorText = withId(R.id.textView_error);
// 2. 화면과 관련된 동작을 메소드로 정의
public LoginPage enterEmail(String email) {
onView(emailField).perform(typeText(email), closeSoftKeyboard());
return this; // 메소드 체이닝을 위해 자기 자신을 반환
}
public LoginPage enterPassword(String password) {
onView(passwordField).perform(typeText(password), closeSoftKeyboard());
return this;
}
public MainPage clickLoginButton_success() {
onView(loginButton).perform(click());
return new MainPage(); // 성공 시 다음 페이지 객체를 반환
}
public LoginPage clickLoginButton_fail() {
onView(loginButton).perform(click());
return this; // 실패 시 현재 페이지에 머무름
}
public LoginPage checkErrorMessage(String message) {
onView(errorText).check(matches(withText(message)));
return this;
}
}
// 실제 테스트 클래스
@Test
public void loginFail_showsErrorMessage() {
LoginPage loginPage = new LoginPage();
loginPage.enterEmail("wrong@email.com")
.enterPassword("wrong_password")
.clickLoginButton_fail()
.checkErrorMessage("Invalid credentials");
}
POM을 사용하면 다음과 같은 장점이 있습니다:
- 가독성 향상: 테스트 코드가
onView,perform같은 Espresso API 대신enterEmail,clickLoginButton과 같은 비즈니스 용어로 작성되어 이해하기 쉬워집니다. - 유지보수 용이성: 만약 로그인 버튼의 ID가 변경되면, 여러 테스트 파일을 수정할 필요 없이
LoginPage클래스 한 곳만 수정하면 됩니다. - 재사용성 증가: 로그인 동작이 필요한 모든 테스트에서
LoginPage객체를 재사용할 수 있습니다.
재사용성 극대화: 커스텀 ViewMatcher와 ViewAction 만들기
프로젝트가 복잡해지면 반복적으로 사용되는 복잡한 Matcher나 Action이 생기기 마련입니다. Espresso는 직접 Matcher<View>나 ViewAction 인터페이스를 구현하여 우리만의 Matcher와 Action을 만들 수 있도록 지원합니다.
예를 들어, TextInputLayout의 에러 텍스트를 확인하는 것은 여러 테스트에서 필요할 수 있습니다. 이를 위한 커스텀 Matcher를 만들 수 있습니다.
public static Matcher<View> hasTextInputLayoutErrorText(final String expectedErrorText) {
return new TypeSafeMatcher<View>() {
@Override
public boolean matchesSafely(View view) {
if (!(view instanceof TextInputLayout)) {
return false;
}
CharSequence error = ((TextInputLayout) view).getError();
if (error == null) {
return false;
}
String hint = error.toString();
return expectedErrorText.equals(hint);
}
@Override
public void describeTo(Description description) {
description.appendText("with error text: " + expectedErrorText);
}
};
}
// 사용법
onView(withId(R.id.textInputLayout_email)).check(matches(hasTextInputLayoutErrorText("이메일 형식이 올바르지 않습니다.")));
이처럼 커스텀 Matcher/Action을 만들면 복잡한 로직을 캡슐화하고, 테스트 코드를 더 간결하고 의미있게 만들 수 있습니다.
테스트 크기 어노테이션(@SmallTest, @MediumTest, @LargeTest) 활용
안드로이드 테스트에서는 테스트의 종류와 실행 시간을 기준으로 테스트를 분류하는 어노테이션을 제공합니다.
@SmallTest: 단위 테스트(Unit test)처럼 외부 의존성 없이 빠르게 실행되는 테스트. 수초 내에 완료되어야 합니다.@MediumTest: 단일 앱 내의 통합 테스트(Integration test). 파일 시스템이나 데이터베이스 접근을 포함할 수 있습니다. Espresso 테스트는 대부분 여기에 해당합니다.@LargeTest: 여러 앱이나 네트워크 통신을 포함하는 종단간(E2E) 테스트. 실행 시간이 오래 걸립니다.
이 어노테이션들을 테스트 메소드나 클래스에 붙여두면, Gradle 명령어의 android.testInstrumentationRunnerArguments.size 옵션을 통해 특정 크기의 테스트만 선택적으로 실행할 수 있습니다.
# Medium 크기의 테스트만 실행
./gradlew connectedAndroidTest -P android.testInstrumentationRunnerArguments.size=medium
이를 활용하여, 코드 커밋 시에는 빠르게 실행되는 @SmallTest와 @MediumTest만 실행하고, 야간 빌드에서는 모든 테스트(@LargeTest 포함)를 실행하는 등 테스트 전략을 효율적으로 구성할 수 있습니다.
0 개의 댓글:
Post a Comment