用 Espresso 驱动 Flutter Android 应用espresso 包原生 UI 集成测试实战指南【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages本篇技术指南围绕 flutter/packages 仓库中的espresso包展开讲解如何把 Google 官方的 Android UI 测试框架 Espresso 与 Flutter 应用打通从依赖安装、明文网络配置、Gradle 测试依赖到编写基于 Matcher/Action/Assertion 的 Java 测试用例、接入integration_testdriver再到本地与 Firebase Test Lab 云端运行。读完本文你将掌握一套可在 CI 中稳定复用的 Flutter Android 原生集成测试方案并能读懂espresso包在仓库中的源码实现与测试骨架。espresso 是什么让 Espresso 与 Flutter 应用对话espresso是 Flutter 官方维护的插件包定位是“Provides bindings for Espresso tests of Flutter Android apps”即为 Flutter Android 应用提供 Espresso 测试绑定见 espresso/README.md。它的价值在于Espresso 本身只能操作原生 Android View无法感知 Flutter 渲染层中的 Widget而espresso包在 Java 侧提供了一组与 Espresso 风格一致的 APIonFlutterWidget、perform、check让测试作者可以用熟悉的 Espresso 写法直接驱动 Flutter Widget例如点击带指定 tooltip 的按钮、校验带指定 ValueKey 的文本。当前包对平台的官方支持情况如下来自 README 支持矩阵平台支持情况AndroidSDK 24也就是说该绑定仅面向 Android且要求应用的minSdk不低于 24。从仓库源码看espresso的 Java 实现集中在 android/src/main/java/androidx/test/espresso/flutter/ 目录下核心入口是 EspressoFlutter.java它通过onFlutterWidget(WidgetMatcher)创建WidgetInteraction再经由perform(WidgetAction...)与check(WidgetAssertion)执行交互与断言。底层通信机制可以推断为测试通过 WebSocket明文流量与运行中的 Flutter 应用建立 JSON-RPC 连接借助 Dart VM Service 协议与 Flutter 侧交互并通过WaitForConditionAction及NoPendingFrameCondition、NoPendingPlatformMessagesCondition、NoTransientCallbacksCondition等条件见 internal/protocol/impl/ 目录保证在 Flutter 处于 idle 状态后才执行操作。这正是 README 要求“测试期间开启明文流量”的根本原因。安装把 espresso 加入 dev_dependency在应用的pubspec.yaml中把espresso作为dev_dependency引入dev_dependencies: espresso: ^0.4.0如果你测试的是某个包的 example app同样需要把espresso加为主包的 dev_dependencyREADME 中的原话要求。仓库中的 example 应用example/pubspec.yaml展示的标准写法如下其中插件与示例应用同仓库时使用path: ../引用当前版本dev_dependencies: espresso: # 真实应用应使用espresso: ^x.y.z path: ../ flutter_driver: sdk: flutter flutter_test: sdk: flutter integration_test: sdk: flutterespresso包自身的 pubspec.yaml 声明了插件信息Android 平台对应package: com.example.espresso、pluginClass: EspressoPlugin当前版本为0.4.026并要求 Dart SDK^3.10.0、Flutter3.38.0。包本身不依赖第三方运行时库仅依赖flutterSDK。启用明文流量Espresso 测试的网络前提Espresso 通过WebSocket 明文流量与 Flutter 应用协调测试README 明确说明因此测试期间必须允许 cleartext 流量。请只在测试场景debug 或 androidTest下开启不要把它带进发布包。第一步在测试用 Android 应用的AndroidManifest.xml的application节点上挂载网络安全配置application android:networkSecurityConfigxml/network_security_config ... /application第二步在res/xml/目录下创建network_security_config.xmlnetwork-security-config !-- Cleartext is needed for Espresso testing. -- base-config cleartextTrafficPermittedtrue /base-config /network-security-config仓库中 espresso example 的真实配置位于 example/android/app/src/debug/res/xml/network_security_config.xml其 debug 专用清单 example/android/app/src/debug/AndroidManifest.xml 内容如下注意它同时声明了INTERNET权限manifest xmlns:androidhttp://schemas.android.com/apk/res/android !-- Flutter needs it to communicate with the running application to allow setting breakpoints, to provide hot reload, etc. -- uses-permission android:nameandroid.permission.INTERNET/ application android:networkSecurityConfigxml/network_security_config / /manifestREADME 特别强调最佳实践是把上述配置放在 debug 或 androidTest 的 AndroidManifest.xml 中这样发布到用户手中的 release 包不会携带明文流量许可。example 应用的做法即是如此配置只出现在src/debug/下。配置 Gradle 测试依赖在android/app/build.gradle.kts中添加如下依赖README 原文代码块仓库示例文件见 example/android/app/build.gradle.ktsdependencies { testImplementation(junit:junit:4.13.2) // ··· api(androidx.test:core:1.6.1) // ··· androidTestImplementation(androidx.test:runner:1.6.1) // ··· androidTestImplementation(com.google.truth:truth:1.1.3) // ··· androidTestImplementation(androidx.test.espresso:espresso-core:3.6.1) // ··· }各依赖的作用对应如下junit:junit:4.13.2本地单元测试与断言基础androidx.test:core:1.6.1ActivityScenario等核心测试 API用api而非implementation是为了让测试源码可见androidx.test:runner:1.6.1AndroidJUnitRunner与 JUnit Rules是 instrumentation 测试的运行载体com.google.truth:truth:1.1.3Truth 断言库仓库的MainActivityTest.java中import static com.google.common.truth.Truth.assertThat;即来自它androidx.test.espresso:espresso-core:3.6.1Espresso 核心其中包含了androidx.test.espresso.flutter.*绑定类。此外example 的 build.gradle.kts 还配置了testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner并额外引入androidx.test:rules:1.6.1、androidx.test.ext:junit:1.2.1、espresso-contrib、espresso-intents、espresso-accessibility、espresso-web、idling-concurrent等可选依赖可按测试需要取舍。编写第一个 Espresso–Flutter 测试创建android/app/src/androidTest目录并在符合包名的子目录下放置测试文件例如android/app/src/androidTest/java/com/example/MainActivityTest.java。README 提供的完整示例仓库 example 中的真实实现位于 example/android/app/src/androidTest/java/io/flutter/com/example/espresso_example/MainActivityTest.javapackage com.example.espresso_example; import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; import static androidx.test.espresso.flutter.action.FlutterActions.click; import static androidx.test.espresso.flutter.action.FlutterActions.syntheticClick; import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches; import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isDescendantOf; import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withTooltip; import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withType; import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import androidx.test.core.app.ActivityScenario; import androidx.test.espresso.flutter.EspressoFlutter.WidgetInteraction; import androidx.test.espresso.flutter.assertion.FlutterAssertions; import androidx.test.espresso.flutter.matcher.FlutterMatchers; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** Unit tests for {link EspressoFlutter}. */ RunWith(AndroidJUnit4.class) public class MainActivityTest { Before public void setUp() throws Exception { ActivityScenario.launch(MainActivity.class); } Test public void performClick() { onFlutterWidget(withTooltip(Increment)).perform(click()); onFlutterWidget(withValueKey(CountText)).check(matches(withText(Button tapped 1 time.))); } }这段测试的语义与仓库 example 应用 example/lib/main.dart 中的计数器页面一一对应FloatingActionButton带tooltip: Increment计数文本挂在ValueKeyString(CountText)上文案为Button tapped $_counter time${_counter 1 ? : s}。因此onFlutterWidget(withTooltip(Increment))在 Flutter Widget 树中定位带“Increment”提示的按钮.perform(click())对其实施一次点击使计数变为 1onFlutterWidget(withValueKey(CountText))定位计数文本.check(matches(withText(Button tapped 1 time.)))校验文案为单数形式。从 EspressoFlutter.java 的源码可以看到这套链式 API 的实现onFlutterWidget内部会用FlutterMatchers.isFlutterView()约束宿主 View构造WidgetInteractionperform会逐个执行传入的WidgetAction并在交互前后通过等待机制确保 Flutter 处于空闲状态check则先抓取匹配 Widget 的WidgetInfo再委托FlutterViewAssertion完成断言。若匹配失败会抛出NoMatchingWidgetException见 exception/ 目录同目录还有AmbiguousWidgetMatcherException、InvalidFlutterViewException分别对应“匹配到多个 Widget”和“未找到 FlutterView”等场景。Matcher / Action / Assertion API 全览espresso在 Java 侧提供了一套与 Espresso 习惯对齐的 Flutter Widget 操作 API全部可从源码确认Matcher定位 Widget定义于 matcher/FlutterMatchers.java方法用途withTooltip(String)按 tooltip 文案匹配 Widget如按钮的tooltip属性withValueKey(String)按ValueKeyString匹配 WidgetwithType(String)按运行时类型匹配如withType(TextField)匹配 Flutter 的TextFieldwithText(String)按文本内容匹配 WidgetisDescendantOf(ancestorMatcher, widgetMatcher)匹配某个祖先 Widget 之下的后代 WidgetisExisting()校验 Widget 存在于 Widget 树中注意只保证在树中不一定在屏幕上可见例如位于 Scrollable 缓存区但未滚入视口isFlutterView()匹配FlutterView内部约束一般无需直接使用Action执行操作对应 action/ 目录中的实现类FlutterActions.click()ClickAction、FlutterActions.syntheticClick()SyntheticClickAction、FlutterScrollToAction滚动到目标 Widget、FlutterTypeTextAction输入文本、WaitUntilIdleAction等待 Flutter 空闲等。Assertion校验结果对应 assertion/FlutterAssertions.java 的FlutterAssertions.matches(matcher)配合上述 matcher 使用。同步与超时交互与断言默认有 10 秒超时DEFAULT_INTERACTION_TIMEOUT见 common/Constants.javaWidgetInteraction还会额外追加 1 秒余量避免在超时边界上误判。真正执行前框架会通过WaitForConditionAction轮询“无待处理帧、无待处理平台消息、无瞬态回调”等条件确保 Flutter 渲染稳定后才操作这与 Espresso 对原生 UI 的 idling 同步理念一脉相承。接入 integration_test 与 driver 脚本为了让flutter drive/Espresso 能够运行你的 Dart 集成测试需要创建一个把控制权交给integration_test包的driver 脚本放在test_driver/目录下例如test_driver/integration_test.dartimport package:integration_test/integration_test_driver.dart; Futurevoid main() integrationDriver();仓库 example 中该文件位于 example/test_driver/integration_test.dart其 Dart 侧集成测试位于 example/integration_test/espresso_launch_test.dart使用IntegrationTestWidgetsFlutterBinding.ensureInitialized()初始化绑定。同时example 的 androidTest 目录下还有另一个关键文件 MainActivityTest.java它展示了与 README 示例不同的另一种接线方式——通过DartIntegrationTest注解配合dev.flutter.plugins.integration_test.FlutterTestRunner与ActivityTestRuleMainActivity运行 Dart 集成测试DartIntegrationTest RunWith(FlutterTestRunner.class) public class MainActivityTest { Rule public ActivityTestRuleMainActivity rule new ActivityTestRule(MainActivity.class, true, false); }两种形态对应两种驱动路径README 示例直接使用EspressoFlutter的 Java API 编写原生 instrumentation 测试example 仓库的DartIntegrationTest形态则让 Espresso 作为 runner 驱动 Dart 侧integration_test用例。你可以根据测试逻辑主要写在 Java 还是 Dart 侧来选择。本地运行测试在 Android 工程的 Gradle 目录下example 中为example/android执行以下命令即可在本机运行测试./gradlew app:connectedAndroidTest -Ptargetpwd/../test_driver/integration_test.dart要点说明-Ptarget指向 Dart 集成测试入口即上一步创建的 driver 脚本路径pwd取的是 Gradle 工程所在目录因此../test_driver/integration_test.dart会解析到应用工程下的test_driver/integration_test.dartconnectedAndroidTest需要已连接的设备或已启动的模拟器Android SDK 24。在 Firebase Test Lab 上运行espresso同样支持把 instrumentation 测试投放到 Firebase Test Lab 云端设备执行README 原命令如下./gradlew app:assembleAndroidTest ./gradlew app:assembleDebug -Ptargetpath_to_test.dart gcloud auth activate-service-account --key-filePATH_TO_KEY_FILE gcloud --quiet config set project PROJECT_NAME gcloud firebase test android run --type instrumentation \ --app build/app/outputs/apk/debug/app-debug.apk \ --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk\ --timeout 2m \ --results-bucketRESULTS_BUCKET \ --results-dirRESULTS_DIRECTORY各步骤含义assembleAndroidTest构建 androidTest APK内含 Espresso/espresso测试与 runnerassembleDebug -Ptarget...以-Ptarget指定的 Dart 集成测试为入口构建被测应用 APKgcloud auth activate-service-account用服务账号密钥文件完成命令行认证gcloud config set project切换到目标 Firebase/GCP 项目gcloud firebase test android run把--app被测应用 APK与--test测试 APK提交到 Firebase Test Lab--timeout 2m限制单次执行时长--results-bucket/--results-dir指定结果存放位置。注意事项与最佳实践明文流量仅限测试构建network_security_config.xml与清单引用务必放在src/debug/或src/androidTest/下避免把cleartextTrafficPermittedtrue泄露到生产 APK。Matcher 需唯一定位一个 matcher 若匹配到多个 Widget 会触发AmbiguousWidgetMatcherException若匹配不到则触发NoMatchingWidgetException定位复杂层级时优先使用withValueKey必要时用isDescendantOf缩小范围。isExisting()的语义边界它只保证 Widget 存在于 Widget 树中不代表已显示在屏幕上例如 Scrollable 未滚入视口的部分做可见性断言时需注意。平台范围espresso仅支持 AndroidSDK 24iOS 等平台请使用integration_test的其他驱动方式。同步机制是稳定性的关键框架内置的 idle 等待等待帧、平台消息、瞬态回调处理完毕能大幅降低测试抖动编写自定义 Action 时也应遵循同样的“先同步、后操作”原则。至此你已掌握espresso包从安装配置到本地/云端运行测试的完整链路并理解了其在 flutter/packages 仓库中的源码实现与两种测试接线形态可据此在真实项目中搭建 Flutter Android 的原生 Espresso 集成测试体系。【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考