Espresso测试套件

testland2026-10-25查看 311复制 864

为 Android 编写 Espresso UI 测试——使用 `onView(withId(...)).perform(...).check(matches(...))`,依赖 Espresso 的自动同步(不使用 `Thread.sleep`),为应用特定的异步接入 `IdlingResource`,通过 `./gradlew connectedAndroidTest` 运行并解析 JUnit XML 输出。当 Android 应用需要在 Google 的第一方框架中进行 UI 测试时使用。

Download (.md)

📘如何使用这个技能

  1. 1
    复制技能内容·点击上方「Copy SKILL.md」复制全部内容,或 Download 下载到本地
  2. 2
    打开你的 AI 客户端·支持 Claude Code、Trae、Cursor、ChatGPT、DeepSeek、Kimi 等任意 AI 助手
  3. 3
    粘贴为系统提示/知识文件·Claude Code:放入 .claude/skills/ 目录;普通对话:作为首条消息发送并说「请严格按这份技能文档执行」
  4. 4
    直接提出你的需求·AI 会按技能定义的工作流、格式和约束来处理,无需再解释背景

💡 提示:技能生效后,每次对话只需一句需求,AI 会按 SKILL.md 的流程执行。

SKILL.md9422 chars
---
name: espresso-suite
description: "Authors Espresso UI tests for Android - uses `onView(withId(...)).perform(...).check(matches(...))`, leans on Espresso's automatic synchronization (no `Thread.sleep`), wires `IdlingResource` for app-specific async, runs via `./gradlew connectedAndroidTest` and parses the JUnit XML output. Use when an Android app needs UI tests in Google's first-party framework."
---

# espresso-suite

## Overview

Per [espresso-doc][esp]:

[esp]: https://developer.android.com/training/testing/espresso

> "Espresso is a framework for writing concise, beautiful, and
> reliable Android UI tests."

The canonical example:

```kotlin
@Test
fun greeterSaysHello() {
    onView(withId(R.id.name_field)).perform(typeText("Steve"))
    onView(withId(R.id.greet_button)).perform(click())
    onView(withText("Hello Steve!")).check(matches(isDisplayed()))
}
```

The shape: `onView(<matcher>).perform(<action>).check(<matches>)`.

The framework auto-waits via "synchronization conditions" per
[espresso-doc][esp]:

> "Each time a test invokes onView(), Espresso waits to perform
> the corresponding UI action or assertion until these
> synchronization conditions are met:
> - The message queue doesn't have any messages that Espresso needs to immediately process
> - There are no instances of AsyncTask currently executing a task
> - All developer-defined idling resources are idle"

This eliminates the need for `Thread.sleep` for stock Android
async work.

## When to use

- An Android app needs UI tests in Google's first-party framework.
- The team is not on cross-platform (Detox / Appium); pure Android
  stack.
- A flake-prone Espresso suite needs the IdlingResource pattern
  to stabilize.

## Step 1 - Add dependencies

```gradle
// app/build.gradle
android {
    defaultConfig {
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

dependencies {
    androidTestImplementation 'androidx.test.ext:junit:1.2.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
    androidTestImplementation 'androidx.test:rules:1.6.1'
}
```

Per [espresso-doc][esp], Espresso ships as multiple packages:

| Package                       | Purpose                                                        |
|-------------------------------|----------------------------------------------------------------|
| `espresso-core`                | Core View matchers, actions, assertions.                       |
| `espresso-web`                 | WebView support.                                                |
| `espresso-idling-resource`     | Synchronization for background jobs.                           |
| `espresso-contrib`             | DatePicker, RecyclerView, Drawer actions, accessibility checks.|
| `espresso-intents`             | Intent validation + stubbing.                                   |
| `espresso-remote`              | Multi-process functionality.                                    |

## Step 2 - Author tests

```kotlin
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

@RunWith(AndroidJUnit4::class)
class CheckoutTest {
    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun applyPromoCode() {
        onView(withId(R.id.promo_field)).perform(typeText("WELCOME10"), closeSoftKeyboard())
        onView(withId(R.id.apply_button)).perform(click())
        onView(withId(R.id.subtotal)).check(matches(withText("$22.49")))
    }
}
```

## Step 3 - IdlingResource for app-specific async

When the app uses something Espresso doesn't auto-track (e.g.
custom thread pool, RxJava chains), wire an IdlingResource:

```kotlin
class NetworkIdlingResource : IdlingResource {
    @Volatile private var callback: IdlingResource.ResourceCallback? = null
    @Volatile private var pendingRequests = 0

    override fun getName() = "NetworkIdlingResource"
    override fun isIdleNow() = pendingRequests == 0
    override fun registerIdleTransitionCallback(cb: IdlingResource.ResourceCallback) {
        callback = cb
    }

    fun increment() = synchronized(this) { pendingRequests++ }
    fun decrement() = synchronized(this) {
        pendingRequests--
        if (pendingRequests == 0) callback?.onTransitionToIdle()
    }
}

// Wire from production code at network call sites:
networkIdlingResource.increment()
api.fetchOrders().enqueue { _, _ -> networkIdlingResource.decrement() }

// Register in test setup:
@Before fun setup() { IdlingRegistry.getInstance().register(networkIdlingResource) }
@After  fun teardown() { IdlingRegistry.getInstance().unregister(networkIdlingResource) }
```

Without IdlingResource, Espresso assumes idle when its built-in
trackers report idle - async work the framework can't see triggers
flake.

## Step 4 - Common matchers and actions

```kotlin
// Matchers
withId(R.id.button)              // by ID
withText("Submit")                // by displayed text
withContentDescription("Login")   // by content description
hasSibling(withText("Email"))     // structural relationship
isDisplayed()                     // currently visible

// Actions
click(), longClick()
typeText("..."), clearText()
scrollTo(), swipeLeft(), swipeUp()
pressBack()

// Assertions
matches(withText("Hello"))
matches(isDisplayed())
matches(isEnabled())
doesNotExist()
```

## Step 5 - RecyclerView interactions

```kotlin
// espresso-contrib provides RecyclerViewActions
onView(withId(R.id.cart_list))
    .perform(RecyclerViewActions.actionOnItemAtPosition<MyViewHolder>(0, click()))

onView(withId(R.id.cart_list))
    .perform(RecyclerViewActions.scrollTo<MyViewHolder>(hasDescendant(withText("BOOK-001"))))
```

## Step 6 - Run

```bash
# Unit + UI tests on connected device / emulator
./gradlew connectedAndroidTest

# With test orchestrator (each test in fresh process - preferred for isolation):
android {
    defaultConfig {
        testInstrumentationRunnerArguments clearPackageData: 'true'
    }
    testOptions {
        execution 'ANDROIDX_TEST_ORCHESTRATOR'
    }
}
```

## Step 7 - CI integration (Android emulator on GitHub Actions)

```yaml
jobs:
  ui-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          script: ./gradlew connectedAndroidTest
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: app/build/outputs/androidTest-results
```

The test results land in JUnit XML at
`app/build/outputs/androidTest-results/.../TEST-*.xml` - 
consumable by `junit-xml-analysis` (in the qa-test-reporting plugin).

## Anti-patterns

| Anti-pattern                                                         | Why it fails                                                              | Fix |
|----------------------------------------------------------------------|---------------------------------------------------------------------------|-----|
| `Thread.sleep` for async waits                                       | Espresso has built-in sync; sleeps mask race conditions.                  | Use IdlingResource for unsupported async (Step 3). |
| `withText("Submit")` for buttons                                      | Translation breaks the test.                                              | `withId(R.id.submit_button)` or `withContentDescription`. |
| Missing `closeSoftKeyboard()` after typing                            | Keyboard obscures next view; `click()` misses.                            | Chain `closeSoftKeyboard()` after `typeText`. |
| Skipping `IdlingResource` for Retrofit / OkHttp                       | Network async invisible; flaky.                                            | Wire IdlingResource (Step 3) OR use OkHttp's IdlingResource bridge. |
| One mega-test that asserts 10 things                                  | One failure obscures others; debugging hard.                              | One assertion target per `@Test`. |
| Skipping test orchestrator                                            | Tests share process; one test's state leaks.                              | Enable `ANDROIDX_TEST_ORCHESTRATOR` (Step 6). |

## Limitations

- **In-process only.** Espresso runs in the app's process; no cross-app
  flows. Use UIAutomator for system-UI / multi-app interactions.
- **No iOS / web.** Single-platform; for cross-platform see
  `appium-testing`.
- **Async opacity.** Custom executors / coroutines need explicit
  IdlingResource integration.
- **Emulator slowness.** Real devices ~2× faster than emulators
  for UI tests; consider device farms (Firebase Test Lab, AWS
  Device Farm) for matrix runs.

## References

- [esp][esp] - Espresso doc: small/predictable API, automatic
  synchronization (message queue / AsyncTask / IdlingResource),
  package list (`-core`, `-web`, `-contrib`, `-idling-resource`,
  `-intents`, `-remote`).
- `xcuitest-suite` - iOS sibling.
- `appium-testing` - cross-platform
  alternative.
- `junit-xml-analysis` - downstream parser.
来源:skillsdirectory

评论0

登录后参与评论

还没有评论,来抢沙发吧