移动测试框架Skill MobileTestingFrameworks

这项技能提供了全面的移动测试框架专业知识,支持端到端、原生和跨平台测试。

测试 0 次安装 0 次浏览 更新于 2/26/2026

name: 移动测试框架 description: 移动测试框架技能 version: 1.0.0 category: 质量保证 slug: mobile-testing status: active

移动测试框架技能

概览

这项技能提供了跨平台移动测试框架的全面专业知识。它支持使用Detox和Maestro进行端到端测试,使用XCUITest和Espresso进行原生测试,以及使用Appium进行跨平台测试。

允许使用的工具

  • bash - 执行测试命令和框架CLIs
  • read - 分析测试文件和配置
  • write - 生成测试用例和配置
  • edit - 更新现有测试
  • glob - 搜索测试文件
  • grep - 在测试代码中搜索模式

能力

Detox (React Native)

  1. 配置

    • 设置Detox配置
    • 配置iOS和Android构建
    • 设置设备/模拟器目标
    • 配置测试运行器(Jest)
  2. 测试编写

    • 编写元素匹配器
    • 实施动作(点击、输入、滚动)
    • 配置期望
    • 处理同步
  3. 高级特性

    • 模拟原生模块
    • 处理权限
    • 配置网络模拟
    • 实施视觉回归

Maestro

  1. 流程配置

    • 编写YAML测试流程
    • 配置应用启动
    • 设置设备目标
    • 处理环境变量
  2. 动作和断言

    • 点击、输入、滑动手势
    • 断言元素可见性
    • 截图
    • 运行JavaScript断言

XCUITest (iOS)

  1. 测试设置

    • 配置测试方案
    • 设置测试计划
    • 配置设备目标
    • 处理启动参数
  2. UI测试

    • 使用XCUIElement进行元素查询
    • 动作(点击、滑动、捏合)
    • 使用辅助功能标识符
    • 键盘处理

Espresso (Android)

  1. 测试配置

    • 设置仪器测试
    • 配置测试运行器
    • 处理Hilt注入
    • 配置Compose测试
  2. UI测试

    • ViewMatchers和ViewActions
    • Compose语义测试
    • IdlingResources用于异步
    • 意图验证

Appium

  1. 跨平台测试
    • 配置能力
    • 设置驱动会话
    • 处理多个平台
    • 配置云测试

设备农场

  1. 云测试
    • AWS设备农场集成
    • Firebase测试实验室设置
    • BrowserStack配置
    • 测试分发

目标流程

这项技能与以下流程集成:

  • mobile-testing-strategy.js - 测试策略实施
  • mobile-accessibility-implementation.js - 可访问性测试
  • mobile-security-implementation.js - 安全测试

依赖

必需

  • Node.js(用于Detox、Maestro)
  • Xcode(用于XCUITest)
  • Android Studio(用于Espresso)
  • 平台特定的SDK

可选

  • Appium
  • 设备农场账户
  • CI/CD平台

配置

Detox配置

// .detoxrc.js
module.exports = {
  testRunner: {
    args: {
      $0: 'jest',
      config: 'e2e/jest.config.js',
    },
    jest: {
      setupTimeout: 120000,
    },
  },
  apps: {
    'ios.debug': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app',
      build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
    },
    'ios.release': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/MyApp.app',
      build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
    },
    'android.debug': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
      reversePorts: [8081],
    },
    'android.release': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
      build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
    },
  },
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: { type: 'iPhone 15 Pro' },
    },
    emulator: {
      type: 'android.emulator',
      device: { avdName: 'Pixel_7_API_34' },
    },
  },
  configurations: {
    'ios.sim.debug': {
      device: 'simulator',
      app: 'ios.debug',
    },
    'ios.sim.release': {
      device: 'simulator',
      app: 'ios.release',
    },
    'android.emu.debug': {
      device: 'emulator',
      app: 'android.debug',
    },
    'android.emu.release': {
      device: 'emulator',
      app: 'android.release',
    },
  },
};

使用示例

Detox测试

// e2e/login.test.ts
import { device, element, by, expect } from 'detox';

describe('登录流程', () => {
  beforeAll(async () => {
    await device.launchApp({
      newInstance: true,
      permissions: { notifications: 'YES' },
    });
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('首次启动应显示登录屏幕', async () => {
    await expect(element(by.id('login-screen'))).toBeVisible();
    await expect(element(by.id('email-input'))).toBeVisible();
    await expect(element(by.id('password-input'))).toBeVisible();
  });

  it('应显示无效凭证的错误', async () => {
    await element(by.id('email-input')).typeText('invalid@example.com');
    await element(by.id('password-input')).typeText('wrongpassword');
    await element(by.id('login-button')).tap();

    await expect(element(by.id('error-message'))).toBeVisible();
    await expect(element(by.text('无效凭证'))).toBeVisible();
  });

  it('成功登录后应导航到主页', async () => {
    await element(by.id('email-input')).typeText('test@example.com');
    await element(by.id('password-input')).typeText('password123');
    await element(by.id('login-button')).tap();

    await waitFor(element(by.id('home-screen')))
      .toBeVisible()
      .withTimeout(5000);

    await expect(element(by.id('welcome-message'))).toBeVisible();
  });

  it('应处理长列表的滚动', async () => {
    // 先登录
    await element(by.id('email-input')).typeText('test@example.com');
    await element(by.id('password-input')).typeText('password123');
    await element(by.id('login-button')).tap();

    await waitFor(element(by.id('home-screen'))).toBeVisible().withTimeout(5000);

    // 滚动到列表底部
    await element(by.id('item-list')).scrollTo('bottom');
    await expect(element(by.id('item-50'))).toBeVisible();

    // 滚动回顶部
    await element(by.id('item-list')).scrollTo('top');
    await expect(element(by.id('item-1'))).toBeVisible();
  });
});

Maestro流程

# flows/login.yaml
appId: com.example.myapp
---
- launchApp:
    clearState: true

- assertVisible: "Welcome"

- tapOn: "Email"
- inputText: "test@example.com"

- tapOn: "Password"
- inputText: "password123"

- tapOn: "Sign In"

- assertVisible: "Home"
- takeScreenshot: "home_after_login"

# 测试登出流程
- tapOn: "Profile"
- tapOn: "Sign Out"
- assertVisible: "Welcome"

XCUITest

// MyAppUITests/LoginTests.swift
import XCTest

final class LoginTests: XCTestCase {
    var app: XCUIApplication!

    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launchArguments = ["--uitesting"]
        app.launch()
    }

    override func tearDownWithError() throws {
        app = nil
    }

    func testLoginScreenElements() throws {
        XCTAssertTrue(app.textFields["email-input"].exists)
        XCTAssertTrue(app.secureTextFields["password-input"].exists)
        XCTAssertTrue(app.buttons["login-button"].exists)
    }

    func testSuccessfulLogin() throws {
        let emailField = app.textFields["email-input"]
        let passwordField = app.secureTextFields["password-input"]
        let loginButton = app.buttons["login-button"]

        emailField.tap()
        emailField.typeText("test@example.com")

        passwordField.tap()
        passwordField.typeText("password123")

        loginButton.tap()

        // 等待主页屏幕
        let homeScreen = app.otherElements["home-screen"]
        XCTAssertTrue(homeScreen.waitForExistence(timeout: 5))
    }

    func testInvalidCredentials() throws {
        app.textFields["email-input"].tap()
        app.textFields["email-input"].typeText("invalid@example.com")

        app.secureTextFields["password-input"].tap()
        app.secureTextFields["password-input"].typeText("wrong")

        app.buttons["login-button"].tap()

        XCTAssertTrue(app.staticTexts["Invalid credentials"].waitForExistence(timeout: 3))
    }

    func testScrollBehavior() throws {
        // 导航到列表屏幕
        app.buttons["list-tab"].tap()

        let list = app.tables["item-list"]
        let lastItem = app.cells["item-cell-50"]

        // 向下滚动
        while !lastItem.isHittable {
            list.swipeUp()
        }

        XCTAssertTrue(lastItem.exists)

        // 向上滚动
        let firstItem = app.cells["item-cell-1"]
        while !firstItem.isHittable {
            list.swipeDown()
        }

        XCTAssertTrue(firstItem.exists)
    }
}

Espresso测试

// app/src/androidTest/java/com/example/myapp/LoginTest.kt
package com.example.myapp

import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class LoginTest {

    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val composeRule = createAndroidComposeRule<MainActivity>()

    @Before
    fun setup() {
        hiltRule.inject()
    }

    @Test
    fun loginScreen_displaysAllElements() {
        composeRule.onNodeWithTag("email-input").assertIsDisplayed()
        composeRule.onNodeWithTag("password-input").assertIsDisplayed()
        composeRule.onNodeWithTag("login-button").assertIsDisplayed()
    }

    @Test
    fun login_withValidCredentials_navigatesToHome() {
        composeRule.onNodeWithTag("email-input")
            .performTextInput("test@example.com")

        composeRule.onNodeWithTag("password-input")
            .performTextInput("password123")

        composeRule.onNodeWithTag("login-button")
            .performClick()

        composeRule.waitUntil(5000) {
            composeRule.onAllNodesWithTag("home-screen")
                .fetchSemanticsNodes().isNotEmpty()
        }

        composeRule.onNodeWithTag("home-screen").assertIsDisplayed()
    }

    @Test
    fun login_withInvalidCredentials_showsError() {
        composeRule.onNodeWithTag("email-input")
            .performTextInput("invalid@example.com")

        composeRule.onNodeWithTag("password-input")
            .performTextInput("wrong")

        composeRule.onNodeWithTag("login-button")
            .performClick()

        composeRule.onNodeWithText("Invalid credentials")
            .assertIsDisplayed()
    }

    @Test
    fun list_scrollsBehavior() {
        // 先登录
        composeRule.onNodeWithTag("email-input")
            .performTextInput("test@example.com")
        composeRule.onNodeWithTag("password-input")
            .performTextInput("password123")
        composeRule.onNodeWithTag("login-button")
            .performClick()

        composeRule.waitUntil(5000) {
            composeRule.onAllNodesWithTag("item-list")
                .fetchSemanticsNodes().isNotEmpty()
        }

        // 滚动到第50项
        composeRule.onNodeWithTag("item-list")
            .performScrollToNode(hasTestTag("item-50"))

        composeRule.onNodeWithTag("item-50").assertIsDisplayed()
    }
}

Firebase测试实验室

# .github/workflows/android-test.yml
name: Android测试

on: [push, pull_request]

jobs:
  instrumented-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 设置JDK
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: 构建APK
        run: |
          ./gradlew assembleDebug assembleDebugAndroidTest

      - name: 认证Google Cloud
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_CREDENTIALS }}

      - name: 在Firebase测试实验室上运行测试
        run: |
          gcloud firebase test android run \
            --type instrumentation \
            --app app/build/outputs/apk/debug/app-debug.apk \
            --test app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
            --device model=Pixel6,version=33 \
            --device model=Pixel7,version=34 \
            --timeout 15m \
            --results-bucket gs://my-test-results

质量门

测试覆盖率

  • 单元测试覆盖率> 80%
  • 集成测试覆盖率> 60%
  • E2E关键路径覆盖率100%

测试可靠性

  • 易变测试率< 2%
  • 测试执行时间< 15分钟
  • 网络测试的重试逻辑

可访问性测试

  • VoiceOver/TalkBack验证
  • 颜色对比度验证
  • 触摸目标大小验证

相关技能

  • accessibility-testing - 专注于可访问性测试
  • mobile-perf - 性能测试
  • fastlane-cicd - CI/CD集成

版本历史

  • 1.0.0 - 初始发布,主要框架支持