XCUITest & Espresso:原生 UI 测试框架

FreeGuideOnline 最新 2026-06-17

XCUITest 与 Espresso:原生 UI 测试框架完全指南

目录

  1. 初识原生 UI 测试
  2. XCUITest:iOS 自动化测试
  3. Espresso:Android 自动化测试
  4. 框架对比:如何选择与搭配
  5. 进阶实践与最佳打法
  6. 常用资源与延伸阅读

初识原生 UI 测试

移动应用的质量离不开自动化测试,而最贴近用户视角的便是 UI 测试。苹果与谷歌分别提供了官方的一流框架:

  • XCUITest —— 集成于 Xcode 的 iOS/tvOS UI 测试框架,基于 XCTest。
  • Espresso —— Android 官方 UI 测试框架,隶属于 Android Testing Support Library。

它们都采用白盒方式,直接运行在设备或模拟器上,能够模拟真实用户操作,并以极快的速度反馈结果。本教程将从零开始,带你掌握这两个框架的核心用法与实战技巧。


XCUITest:iOS 自动化测试

环境配置与项目集成

XCUITest 随 Xcode 一起提供,无需额外安装。集成步骤:

  1. 在 Xcode 中打开你的项目。
  2. 选择 File → New → Target,在测试分类中选择 UI Testing Bundle
  3. 为 Target 命名(例如 MyAppUITests),并确保其关联到主应用 Target。
  4. Xcode 自动生成一个继承自 XCTestCase 的测试类,其中包含 setUptearDown 和示例方法。

在测试类头部,你会看到自动导入的 XCTest,UI 测试的入口是通过 XCUIApplication 对象启动应用。

编写你的第一个 XCUITest

下面是一个简单的测试:启动应用,检查登录按钮是否存在,然后模拟点击。

import XCTest

final class LoginUITests: XCTestCase {
    let app = XCUIApplication()

    override func setUp() {
        continueAfterFailure = false
        app.launch()
    }

    func testLoginButtonExists() {
        let loginButton = app.buttons["登录"]
        XCTAssertTrue(loginButton.exists)
    }

    func testTapLoginOpensNextScreen() {
        app.buttons["登录"].tap()
        let welcomeLabel = app.staticTexts["欢迎回来"]
        XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 5))
    }
}

setup 中的 continueAfterFailure = false 表示一旦断言失败即停止当前用例,避免后续步骤因状态错乱而误报。

核心查询与交互 API

XCUITest 使用 查询链 来定位元素,所有 UI 元素通过 XCUIElementQuery 访问:

  • 按类型查询app.buttonsapp.staticTextsapp.textFieldsapp.tables 等。
  • 按标识查询:通过 Accessibility Identifier 或文本标签过滤。
    app.buttons["登录"]
    app.textFields.matching(identifier: "usernameField").firstMatch
    
  • 按层级查询:使用下标或 children(matching:) 深入视图层级。
    let firstCell = app.tables.cells.element(boundBy: 0)
    

常用交互操作:

  • tap() – 点击
  • doubleTap() – 双击
  • swipeUp() / swipeDown() / swipeLeft() / swipeRight() – 滑动
  • typeText("内容") – 输入文本(仅对输入框有效)
  • press(forDuration: 2) – 长按

断言 直接使用 XCTest 的断言函数:

  • XCTAssertTrue(element.exists) – 元素存在
  • XCTAssertEqual(element.value as? String, "期望值") – 值验证
  • XCTAssertFalse(element.isEnabled) – 禁用状态

等待机制与异步处理

UI 操作常需要等待动画或网络请求完成。XCUITest 提供同步等待方法:

  • waitForExistence(timeout: 5) – 等待元素出现,返回 Bool
  • 使用 XCTNSPredicateExpectation 等更复杂的期待。
let exists = NSPredicate(format: "exists == true")
let expectation = XCTNSPredicateExpectation(predicate: exists, object: element)
XCTWaiter().wait(for: [expectation], timeout: 5)

对于网络请求、数据库写入等异步操作,需要在被测代码中插入 XCTest 代理点,例如通过 UIInterruptionMonitor 处理系统弹窗(如权限请求)。

录制与调试技巧

Xcode 提供了 UI Test Recorder,可以录制你的手动操作并生成代码:将光标放在测试方法中,点击录制按钮(红色圆点),然后在 App 界面中操作,Xcode 会自动生成对应的 XCUITest 代码。

调试建议:

  • 使用 po app 查看当前 UI 树。
  • 打印元素:po app.debugDescription
  • 利用 Xcode Accessibility Inspector 查看视图的 Accessibility 属性,这是 XCUITest 定位元素的基石。

Espresso:Android 自动化测试

环境搭建与依赖引入

Espresso 内置于 Android Testing Support Library,只需在模块级 build.gradle 中添加依赖:

android {
    defaultConfig {
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

dependencies {
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    // 可选:意图测试、WebView、贡献库等
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
    androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
}

测试类应放在 app/src/androidTest/java/ 目录下,并使用 @RunWith(AndroidJUnit4::class) 注解(如果是 Kotlin,还可使用 ActivityScenarioActivityTestRule)。

第一个 Espresso 测试

下面测试一个登录界面的按钮行为:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.*
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
import org.junit.Rule
import org.junit.Test

class LoginTest {
    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun loginButton_displaysCorrectText() {
        onView(withId(R.id.login_button))
            .check(matches(withText("登录")))
    }

    @Test
    fun tapLogin_navigatesToWelcome() {
        onView(withId(R.id.username_field)).perform(typeText("user"), closeSoftKeyboard())
        onView(withId(R.id.password_field)).perform(typeText("pass"), closeSoftKeyboard())
        onView(withId(R.id.login_button)).perform(click())
        onView(withText("欢迎回来")).check(matches(isDisplayed()))
    }
}

三大核心组件:Matcher、Action、Assertion

Espresso 的 DSL 遵循 onView(Matcher) -> perform(Action) -> check(Assertion) 模式:

  • ViewMatcher:定位视图,常用包括:
    • withId(R.id.xxx)
    • withText("文字")
    • withContentDescription("描述")
    • isDisplayed()isEnabled()isRoot() 等。
    • 可组合使用:allOf(withId(...), withText(...))
  • ViewAction:模拟用户操作,如 click()longClick()typeText()clearText()swipeLeft()scrollTo() 等。
  • ViewAssertion:检查视图状态,matches(Matcher) 是核心,传入期望的 ViewMatcher。

处理 RecyclerView 时需使用 onView(withId(R.id.recycler)) 并配合 RecyclerViewActions

onView(withId(R.id.recycler))
    .perform(RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(0, click()))

同步保障:空闲资源与 IdlingResource

Espresso 最大的优势是自动同步。它会在主线程空闲、没有动画、没有后台 AsyncTask 等条件下自动等待,省去手动 sleep。但仍有一些情况需要手动告知:

  • IdlingResource:当应用使用自定义异步逻辑(如 RxJava、协程、自定义线程池)时,需要实现 IdlingResource 接口并注册,确保 Espresso 等待操作结束。
  • 常用库已有现成集成,如 RxIdlerCoroutineIdlingResource
  • 示例:在测试开始前通过 IdlingRegistry.getInstance().register(resource) 注册,结束后反注册。

意图测试与 WebView 交互

Intent 测试 需要添加 espresso-intents 依赖:

  • 使用 Intents.init() 初始化,测试结束时调用 Intents.release()
  • intended(hasComponent(...)) 验证是否触发了某个 Intent。
  • intending(hasComponent(...)).respondWith(...) 提供模拟响应。

WebView 交互 需要 espresso-web

onWebView()
    .withElement(findElement(Locator.ID, "login_btn"))
    .perform(webClick())

这在测试混合应用时非常有用。


框架对比:如何选择与搭配

特性 XCUITest (iOS) Espresso (Android)
语言 Swift / Objective-C Kotlin / Java
运行环境 Xcode + 模拟器/真机 Android Studio + 模拟器/真机
同步机制 手动等待(waitForExistence) 自动同步 + IdlingResource
定位方式 Accessibility 层次查询 ViewMatcher(ID、文本等)
特殊功能 录制回放、多应用交互 Intent 打桩、WebView 测试
学习曲线 相对平缓,依赖 Xcode 工具 稍陡,需要理解匹配器与线程同步
CI 集成 xcodebuild test 配合 Fastlane ./gradlew connectedAndroidTest
  • 如果你只开发 iOS,XCUITest 是最自然的选择,原生深度集成,且无需配置其他服务。
  • 如果你只开发 Android,Espresso 提供极致稳定和快速的测试体验,同步特性极大减少不稳定测试。
  • 同时负责双平台,两者均为原生方案,可以共享共同的页面对象设计理念,结合 Appium 这样的跨平台框架仅用于少数场景。

进阶实践与最佳打法

页面对象模式(Page Object)

将 UI 元素和操作封装在独立的类中,提高可维护性。

iOS — XCUITest 示例:

class LoginScreen {
    let app: XCUIApplication
    init(app: XCUIApplication) { self.app = app }

    var usernameField: XCUIElement { app.textFields["用户名"] }
    var loginButton: XCUIElement { app.buttons["登录"] }

    func login(user: String, pass: String) {
        usernameField.tap()
        usernameField.typeText(user)
        app.secureTextFields["密码"].tap()
        app.secureTextFields["密码"].typeText(pass)
        loginButton.tap()
    }
}

Android — Espresso 示例:

class LoginScreen {
    fun setUsername(user: String) {
        onView(withId(R.id.username)).perform(typeText(user), closeSoftKeyboard())
    }
    fun clickLogin() {
        onView(withId(R.id.login_button)).perform(click())
    }
}

提升测试可靠性

  • 使用唯一标识:iOS 设置 accessibilityIdentifier,Android 设置 idtag,避免依赖文本或多语言影响。
  • 避免时序假设:不写 Thread.sleep(),善用框架的等待机制。
  • 管理测试数据:每次测试前重置应用状态,利用 launchArguments(iOS)或测试专属的 Instrumentation(Android)清空数据库。
  • 隔离外部依赖:使用 Mock 服务或网络拦截(如 OHHTTPStubsMockWebServer)。

CI/CD 集成

iOS

xcodebuild test -workspace MyApp.xcworkspace -scheme MyAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 14' \
-derivedDataPath ./build

结合 Fastlane 的 scan 动作可生成报告。

Android

./gradlew connectedAndroidTest

在 CI 中启动模拟器后运行。常见做法是使用 Docker 容器或 Google Cloud Test Lab 执行大规模测试。


常用资源与延伸阅读

无论你选择哪个平台,原生 UI 测试框架都是保障应用质量的最前线。从今天起,为你的核心流程添加一个冒烟测试,感受自动化的力量。