Flutter 插件测试实战指南:四类测试的目录约定、运行命令与测试脚手架搭建

Flutter 插件测试实战指南:四类测试的目录约定、运行命令与测试脚手架搭建 Flutter 插件测试实战指南四类测试的目录约定、运行命令与测试脚手架搭建【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter本篇基于 Flutter 仓库的 Plugin-Tests 文档 展开系统讲解 Flutter 插件plugin体系中 Dart 单元测试、集成测试、原生单元测试、原生 UI 测试四类测试的适用场景与标准存放位置并结合 integration_test 包的源码说明集成测试在真机上运行的底层机制。读完后你能够根据插件的技术构成选择合适的测试类型用仓库提供的命令跑通各类测试并在为插件提交 PR 时按官方规范补齐测试与测试脚手架。为什么插件的测试比一般 Flutter 包更复杂插件与纯 Dart 包的关键区别在于同时包含原生native代码一个完整的插件通常由面向应用的 Dart 包、平台接口包platform interface、方法通道method channel实现以及 Android/iOS/macOS/Windows/Linux/Web 各平台原生实现共同组成。测试策略必须同时覆盖 Dart 层逻辑与原生层行为且不同平台使用的测试框架各不相同JUnit、XCTest、Swift Testing、Google Test、Espresso、XCUITest。因此在写测试之前先理解本仓库的 插件与包仓库结构 是必要的准备工作。四类测试适用场景与标准目录约定1. Dart 单元测试Dart unit tests几乎所有插件都应当拥有 Dart 单元测试唯一的例外是仅包含原生代码只实现一个共享方法通道的联邦实现包federated implementation package。根据插件的组织形态单元测试覆盖对象不同插件形态单元测试覆盖内容Mock 手段单体插件monolithic plugin插件内的 Dart 代码mock 方法通道面向应用的插件包app-facing packageDart 代码mock 平台接口实现含共享方法通道实现的平台接口包平台接口包的 Dart 代码mock 方法通道含 Dart 代码的实现包实现包自身的 Dart 代码部分联邦实现是纯 Dart主要靠单元测试覆盖视情况而定标准位置test/目录。2. 集成测试Integration tests集成测试基于 integration_test 包。与 Dart 单元测试不同集成测试运行在 Flutter 应用即插件的example/示例应用上下文中因此可以加载并实际驱动原生插件代码。除平台接口包外几乎所有插件包都应具备集成测试例外是需要原生 UI 测试的插件见第 4 类纯 Dart 实现、可以完全用 Dart 单元测试覆盖的插件。标准位置example/integration_tests/目录。从本仓库源码可以看到集成测试的工作机制IntegrationTestWidgetsFlutterBinding 继承自LiveTestWidgetsFlutterBinding在真机上以“活绑定”方式运行测试测试结束后它通过 MethodChannel plugins.flutter.io/integration_test 调用allTestsFinished把每条用例的结果成功或Failure详情回传给原生侧Android instrumentation / XCTest。是否回传由环境变量INTEGRATION_TEST_SHOULD_REPORT_RESULTS_TO_NATIVE控制——通过flutter test integration_test运行时由 Flutter 工具自行收集结果该回传会被禁用。此外 binding 还通过 takeScreenshot、traceAction 等 API 提供截图与性能追踪能力。3. 原生单元测试Native unit tests含有原生代码的插件应当为原生代码编写单元测试文档指出目前许多插件尚未补齐这正是插件团队当前的工作重点。各平台的测试框架与标准位置如下平台测试框架标准位置命名约定AndroidJUnitandroid/src/test/—iOSXCTestObj-C或 Swift TestingSwiftexample/ios/RunnerTests/共享 macOS 源码的插件放darwin/RunnerTests/—macOSXCTestObj-C或 Swift TestingSwiftexample/macos/RunnerTests/共享 iOS 源码的插件放darwin/RunnerTests/—LinuxGoogle Testlinux/test/*_test.ccWindowsGoogle Testwindows/test/*_test.cpp单平台项目之所以把测试放在example/目录下是因为它们经由示例应用example app的 Xcode 工程运行。本仓库中的 integration_test 包本身就是符合该约定的实例其 JUnit 测试位于 android/src/test/iOS 测试位于 example/ios/RunnerTests/。4. 原生 UI 测试Native UI tests部分插件会展示必须由测试交互操作的原生 UI例如image_picker。这类场景下普通集成测试无法工作——因为无法从 Dart 侧驱动原生 UI。各平台方案平台测试框架标准位置AndroidEspresso经由 pub.dev 上的espresso插件example/android/app/src/androidTest/iOSXCUITestexample/ios/RunnerUITests/共享 macOS 源码的插件放darwin/RunnerUITests/macOSXCUITestexample/macos/RunnerUITests/共享 iOS 源码的插件放darwin/RunnerUITests/Windows / Linux暂未确定TBD跟踪上游 issue #70233Windows与 #70235Linux—如何运行各类测试运行 Dart 单元测试Dart 单元测试的运行方式与普通 Flutter 单元测试完全一致可以从任意 Flutter IDE 中运行或使用flutter test。唯一例外是*_web包的单元测试必须加上平台参数flutter test --platformchrome也可以使用插件仓库工具中的drive-examples命令批量运行dart run script/tool/bin/flutter_plugin_tools.dart drive-examples --packagesname_of_plugin说明script/tool/bin/flutter_plugin_tools.dart属于插件包仓库flutter/packages的工具链文档原文引用了该工具的使用方式本文所在的 monorepo 中以packages/目录组织插件可直接使用下面的原生命令。运行集成测试方式一通过flutter test在插件目录下cd example flutter test integration_test可以附加-d device参数指定目标设备。方式二从仓库根目录使用drive-examples工具dart run script/tool/bin/flutter_plugin_tools.dart drive-examples --packagesname_of_plugin --platform方式三以 Android instrumentation 测试方式在本地真机上运行cd example flutter build apk cd android ./gradlew -Ptarget$(pwd)/../test_driver/name_of_plugin_test.dart app:connectedAndroidTest本仓库 integration_test 示例 中的androidTest目录展示了该方式所需的 instrumentation 入口文件形态一个使用FlutterTestRunner运行器dev.flutter.plugins.integration_test.FlutterTestRunner的FlutterActivityTest通过ActivityTestRule拉起 Flutter Activity。运行原生测试方式一仓库工具native-test命令在仓库根目录的终端中执行支持平台与测试类别的组合过滤# 同时运行 Android 与 iOS 的单元测试 集成UI测试 dart run script/tool/bin/flutter_plugin_tools.dart native-test --android --ios --packagessome_plugin_name # 仅运行 iOS 的集成测试--no-unit 跳过单元测试 dart run script/tool/bin/flutter_plugin_tools.dart native-test --ios --no-unit --packagessome_plugin_name # 仅运行 Android 的单元测试--no-integration 跳过集成测试 dart run script/tool/bin/flutter_plugin_tools.dart native-test --android --no-integration --packagessome_plugin_name方式二从各原生 IDE 直接运行JUnit将 example 应用作为 Android 工程在 Android Studio 中打开后运行Swift Testing / XCTest / XCUITest将 example 应用作为 Xcode 工程打开后运行Google TestWindowsVisual Studio 会自动检测测试并按常规方式运行。Android 原生测试过滤native-test底层通过调用 Gradle 命令执行测试其运行日志中会打印实际执行的命令形如/path/to/gradlew app:testDebugUnitTest package_name_here:testDebugUnitTest手动执行该命令并追加--tests参数即可按 Gradle 的测试过滤规则只运行指定测试类。例如只运行google_maps_flutter_android中ConvertTest.java的测试/path/to/gradlew app:testDebugUnitTest google_maps_flutter_android:testDebugUnitTest --tests io.flutter.plugins.googlemaps.ConvertTest运行 Web 测试Web 测试大多写作集成测试因为它们需要真实浏览器如 Chrome环境。Web 集成测试位于plugin_name_web包的example目录中。运行步骤确认本机运行的 Chrome 版本下载并安装与该版本匹配的 ChromeDriver启动 ChromeDriverchromedriver --port4444运行测试全部运行从plugins根目录执行dart run script/tool/bin/flutter_plugin_tools.dart drive-examples --packagesplugin_name/plugin_name_web --web单条运行进入该包的example目录后执行flutter drive -d web-server --web-port 7357 --browser-name chrome --driver test_driver/integration_test.dart --target integration_test/NAME_OF_YOUR_test.dart所有 Web 包都在包根目录包含标准的test目录可以用flutter test直接运行。多数情况下这些测试只是引导用户去运行example目录中的集成测试个别包如file_selector_web的该目录包含真正可在 Dart VM 上运行的、非 Web 专属的单元测试。维护 Mock 文件部分包如google_maps_flutter_web在测试文件旁带有.mocks.dart文件。这些 Mock 文件由package:mockito生成其内容会随测试中对 mock 的使用方式变化以及被 mock API 的变化而变化必须通过以下命令更新dart run build_runner build集成测试的代码形态从 integration_test 包源码看端到端流程结合本仓库源码一个完整的集成测试由三部分组成integration_test 示例工程 是最直接的参照包根目录的integration_test/测试文件。以 example_test.dart 为例入口固定为先调用IntegrationTestWidgetsFlutterBinding.ensureInitialized()再执行testWidgets用例。注意该示例使用了条件导入import _example_test_io.dart if (dart.library.js_interop) _example_test_web.dart as tests;即同一入口按运行时区分 iOS/Android 与 Web 两套用例——这正是 Web 集成测试“位于 web 包 example 目录”约定的落地方式。test_driver/目录下的驱动脚本。最小形态为 integration_test.dartimport package:integration_test/integration_test_driver.dart; Futurevoid main() integrationDriver(writeResponseOnFailure: true);需要自定义行为如收集失败时的响应数据时可扩展integrationDriver的参数。平台侧的测试宿主Android 是android/app/src/androidTest/下带FlutterTestRunner的 Activity 测试类如 FlutterActivityTest.javaiOS 是在 Xcode 中新建RunnerTests测试 target用宏INTEGRATION_TEST_IOS_RUNNER(RunnerTests)挂载用例见 RunnerTests.m 与 README 的 iOS 章节。从 binding 的 tearDownAll 逻辑 可以看到当原生插件未被检测到MissingPluginException时会打印明确警告提示你是该用flutter test还是补全插件配置——这是排查“测试跑不起来/结果不回收”问题的第一现场。为 PR 添加测试类型选择与决策模式按 仓库的测试规范Tree-hygiene 中的 Tests 一节任何修改插件的 PR 都应补充测试。需要添加哪一类测试取决于你具体改了什么若拿不准可以在 PR 中或 Discord 的#hackers-ecosystem频道见 Chat 文档提问。多数插件已有的测试脚手架足以支撑你直接在既有文件中追加用例。FAQ我要改的代码本身还没有测试是否也要补测试需要。相当一部分插件代码早于当前的严格测试政策覆盖率本应更高。严格执行新政策的正是为了让整体状况逐步改善。各类测试的添加原则测试应尽量覆盖你新增的全部代码。例如改动涉及错误分支时正常路径与错误路径都应测试。单元测试比集成测试更可靠、运行更快、定位 bug 更精准因此任何非平凡逻辑都应当有单元测试。新功能一般应包含集成测试以验证功能端到端可用个别场景下在完整集成测试中断言原生代码行为并不现实。分类型的具体指引Dart 单元测试只要改了 Dart 代码几乎总是应当补 Dart 单元测试——它们运行快、编写和维护容易。即便你的 Dart 改动只是把新参数从应用面向包传递到平台接口、或从平台接口实现传递到方法通道单元测试也能轻易验证参数在每一层被正确传递。注意除非你的改动完全没有原生代码变更否则仅靠 Dart 单元测试是不够的——Dart 单元测试中不会运行任何原生代码。Dart 集成测试这是插件唯一的完整端到端测试应尽可能编写它是唯一能按插件客户端真实运行方式验证整个流程的测试。但以下场景不可行测试需要与原生 UI 交互如先按下模态对话框中的按钮测试才能继续测试需要检查原生 UI如修改原生代码提供的 platform view 内部视图的属性——虽然个别情况下可以只为测试写一个能通过 Dart 调用原生代码查询这些属性的特殊平台接口测试依赖特定硬件状态。原生单元测试这是单元测试式覆盖原生代码的唯一途径如果原生代码非平凡就应当有原生单元测试。相比完整集成测试原生单元测试往往更容易覆盖全部边界情况如果需要 mock 系统组件如硬件状态只能靠原生单元测试。原生集成测试通常最难维护既按平台拆分、又比单元测试更易 flaky应谨慎使用但它是在需要交互原生 UI 时获得端到端测试的唯一方式。综合以上原则文档给出了几种典型决策模式若 Dart 集成测试可行用它保证功能的端到端行为再用 Dart 和/或原生单元测试覆盖具体实现细节。若测试需要交互原生 UI如选图、选文件用原生集成测试验证整体流程另加单元测试覆盖具体细节或用“stub 掉 UI API、直接以合成调用进入方法通道入口并检查响应”的原生单元测试端到端测试原生部分再配合 Dart 单元测试验证从 Dart API 到方法调用的整条传递链或用方案 1 验证基础功能用方案 2 覆盖那些难以直接测试的边界情况。若测试需要 mock 设备状态如摄像头采用上述方案 2 的组合——stub 掉 UI API 的原生单元测试 验证整条传递链的 Dart 单元测试。补齐测试脚手架Adding test scaffolding如果目标插件缺少你想添加的测试类型对应的脚手架有三种选择若启用方式简单且你有把握可以直接在你的 PR 中一并启用若启用较为复杂但你有把握可以另开一个 PR用一条最小测试先启用脚手架待其评审合入后再继续你的 PR若你没有把握通过 Discord 在#hackers-ecosystem频道求助。无论采用哪种方式若一个“搭建新测试类型”的 PR 两周内未获评审请主动在 Discord 上催办——补齐测试脚手架缺口是团队的高优先级事项。以下为搭建脚手架的具体步骤注意官方说明此处尚未覆盖所有测试类型。启用 XCUITestiOS / macOS用 Xcode 打开path_to_plugin/example/ios/Runner.xcworkspacemacOS 插件将ios替换为macos新建一个 “UI Testing Bundle”在 target 选项窗中按如下填写后点击 “Finish”Product NameRunnerUITestsTeam选择NoneOrganization Identifierdev.flutter.pluginsLanguageSwiftProject选择蓝色的 xcodeproj “Runner”Target to be Tested选择白色的 xcworkspace “Runner”在Build Settings页移除模板生成的大多数 target 级覆盖项特别是CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER目前与 Flutter 不兼容IPHONEOS_DEPLOYMENT_TARGET和TARGETED_DEVICE_FAMILY可能导致测试运行出问题编译器设置CLANG_*、GCC_*、MTL_*不应保留。此时应已生成RunnerUITests文件夹可以直接在其中新增的.swift文件里开始编写用例。启用 Android UI 测试从另一个插件复制DartIntegrationTests.java文件放到example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java在example/android/app/src/androidTest/java/下创建子路径对应 example/android/app/src/main/AndroidManifest.xml 中示例应用包标识符的路径。文件名应为FlutterActivityTest.java若示例的AndroidManifest.xml用自定义MainActivity作为android:name则命名为MainActivityTest.java。例如若AndroidManifest.xml的包标识符是io.flutter.plugins.fooexample、android:name是io.flutter.embedding.android.FlutterActivity则文件路径应为example/android/app/src/androidTest/java/io/flutter/plugins/fooexample/FlutterActivityTest.java。文件内容package io.flutter.plugins.fooexample; import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; DartIntegrationTest RunWith(FlutterTestRunner.class) public class FlutterActivityTest { Rule public ActivityTestRuleFlutterActivity rule new ActivityTestRule(FlutterActivity.class); }注意将package改为实际包名若使用自定义MainActivity把代码中的FlutterActivity替换为MainActivity。确认example/android/app/build.gradle的defaultConfig段包含testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner小结Flutter 插件测试体系的核心可以归纳为一张“类型—位置”映射表Dart 单元测试放test/*_web包需--platformchrome集成测试放example/integration_tests/并依托 integration_test 包 的绑定与plugins.flutter.io/integration_test方法通道回收结果原生单元测试按平台放入android/src/test/、RunnerTests/、linux/test/、windows/test/等约定位置原生 UI 测试放入androidTest/与RunnerUITests/。运行侧则对应flutter test、flutter drive、Gradle instrumentation、native-test/drive-examples工具命令与各原生 IDE 五条通路。在为插件提交 PR 时遵循“先补测试哪怕存量未覆盖、能端到端优先端到端、原生细节靠原生单元测试兜底”的原则并按上文步骤为缺失脚手架的插件补齐对应测试类型即可与仓库当前的测试政策保持一致。【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考