惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News | PayPal Newsroom
云风的 BLOG
云风的 BLOG
GbyAI
GbyAI
Engineering at Meta
Engineering at Meta
B
Blog RSS Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Register - Security
The Register - Security
L
LangChain Blog
A
About on SuperTechFans
S
Schneier on Security
博客园 - 三生石上(FineUI控件)
Stack Overflow Blog
Stack Overflow Blog
The Hacker News
The Hacker News
AWS News Blog
AWS News Blog
博客园 - 司徒正美
Scott Helme
Scott Helme
K
Kaspersky official blog
Cyberwarzone
Cyberwarzone
T
Tenable Blog
腾讯CDC
Recorded Future
Recorded Future
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
G
GRAHAM CLULEY
Security Latest
Security Latest
S
Securelist
D
Darknet – Hacking Tools, Hacker News & Cyber Security
aimingoo的专栏
aimingoo的专栏
Google DeepMind News
Google DeepMind News
V
Vulnerabilities – Threatpost
雷峰网
雷峰网
T
The Exploit Database - CXSecurity.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
T
The Blog of Author Tim Ferriss
D
Docker
S
Security Affairs
F
Full Disclosure
Know Your Adversary
Know Your Adversary
N
News and Events Feed by Topic
N
News and Events Feed by Topic
T
Tor Project blog
Hugging Face - Blog
Hugging Face - Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Microsoft Security Blog
Microsoft Security Blog
Simon Willison's Weblog
Simon Willison's Weblog
Recent Announcements
Recent Announcements
博客园_首页
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
Security @ Cisco Blogs

阿尔的代码屋 | 全栈技术笔记

typing_extensions 有用(四):使用 TypeIs 替代危险的 cast,做最严谨的类型收窄 | 阿尔的代码屋 GoRouter 结合 Isar 运行 Widget 测试并发/粘性线程死锁卡死排坑笔记 | 阿尔的代码屋 typing_extensions 有用(三):使用 Unpack 结合 TypedDict 给 **kwargs 装上透视眼 | 阿尔的代码屋 typing_extensions 有用(二):使用 @override 打造重构代码时的“防呆神器” | 阿尔的代码屋 Flutter 并发测试踩坑实录 - IsarCore 动态库下载冲突与 Widget 测试 HTTP 拦截全链路解决 | 阿尔的代码屋 typing_extensions 有用(一):使用 Self 终结继承时的类型推断灾难 | 阿尔的代码屋 基于 Cloudflare Pages 的纯前端 WebAssembly 应用自动化部署实践 | 开发日志 | 阿尔的代码屋 基于 VS Code 远程开发的 GPU Docker 容器自动清理方案实践 开发日志| 阿尔的代码屋 Patrol iOS 集成测试排坑实录 - xcodebuild exit code 70 全链路解决 | 阿尔的代码屋 Linux/macOS 下 micromamba 报错 Shard Index not available 与极度卡顿 排坑笔记 | 阿尔的代码屋 critical libmamba Shell not initialized micromamba报错 subprocess 无法修改父 Shell - 排坑笔记 | 阿尔的代码屋 VS Code Remote-SSH: 彻底告别重复输入密码 - 排坑笔记 | 阿尔的代码屋 Git 报警: inexact rename detection was skipped - 排坑笔记 | 阿尔的代码屋 解决群晖 NAS SSL 证书自动部署难题 | 阿尔的代码屋 Windows 下 Git error: fatal: refusing to work with credential missing host field - 排坑笔记 | 阿尔的代码屋 VS Code 调试 Python 第三方库源码的配置 - 开发技巧| 阿尔的代码屋 VS Code Remote Tunnels本地连接调试Kaggle GPU - 开发技巧| 阿尔的代码屋 Windows下Git error: open(xxxx) Filename too long - 排坑笔记 | 阿尔的代码屋 记录2026年零预约港卡开户实录避坑 告别孤岛:拥抱 MCP 协议,为大模型打造标准“USB 接口” - 大模型实战 08 完结篇 | 阿尔的代码屋 从 ReAct 到 Workflow:基于云端 API 构建事件驱动的智能体 - 大模型实战 07 额外篇 | 阿尔的代码屋 基于 LlamaIndex ReAct 框架手搓全自动博客监控 Agent - 大模型实战 07 | 阿尔的代码屋 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋 大模型微调实战详解 - 大模型实战 05 | 阿尔的代码屋 基于 ChromaDB 打造工程级 RAG 系统 - 大模型实战 04 | 阿尔的代码屋 拆解 Transformers 原理与图解 - 大模型实战 03 | 阿尔的代码屋 云端炼丹房 2:Kaggle 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 云端炼丹房:Google Colab 免费 GPU 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 Open WebUI 部署与 RAG 实战 - 大模型实战 02 | 阿尔的代码屋
Flutter E2E 测试从 integration_test 迁移到 Patrol - 实践笔记 | 阿尔的代码屋
Algieba · 2026-05-30 · via 阿尔的代码屋 | 全栈技术笔记

核心摘要 (TL;DR)

  • 背景:为 UseUp 项目实现全面 E2E 测试,从 integration_test 迁移到 Patrol(支持原生 OS 交互)。
  • 架构:模块化测试结构(Dashboard、Inventory、Settings、History),共享初始化 helper。
  • 核心问题:Isar “Instance has already been opened” 错误、patrol_cli 版本不兼容、物品保存需过期日期、本地化文本匹配。
  • 关键解法:每个模块单 patrolTest、Isar 单例复用、添加过期日期选择、使用 findsWidgets + .first、修正本地化文本。

问题概览卡片

基本信息

  • 项目背景:Flutter 项目使用 Riverpod + GoRouter + Isar,需要全面 E2E 测试。
  • 技术栈:Flutter 3.41.1, Patrol 3.20.0, patrol_cli 3.11.0, Isar 3.3.0-dev.1
  • 测试目标:Dashboard、Inventory(增删改查)、Settings、History 四大模块
  • 运行环境:Android 模拟器 (emulator-5554), iOS 模拟器 (iPhone 16e)

错误日志复现

1
2
3
4
5
6
7
8
9
10
11
12
13
# 问题 1: Patrol 版本不兼容
Version incompatibility detected! patrol_cli 4.4.0 is not compatible with patrol 3.20.0.

# 问题 2: Android 依赖冲突
Cannot find a version of 'androidx.test:runner' that satisfies the version constraints:
Dependency path 'android:app:unspecified' --> 'androidx.test:runner:1.6.2'
Constraint path 'android:app:unspecified' --> 'androidx.test:runner:{strictly 1.5.1}'

# 问题 3: Isar 实例冲突
IsarException: Instance has already been opened

# 问题 4: Patrol 测试执行失败 (500 Internal Server Error)
pl.leancode.patrol.contracts.PatrolAppServiceClientException: Invalid response 500

1. 现象描述与现场还原

初始尝试:integration_test 的局限

最初使用 integration_test 包编写 E2E 测试,但遇到以下问题:

  • Isar 数据库在测试环境多次初始化时报错 “Instance has already been opened”
  • 无法测试原生 OS 交互(权限对话框、文件选择器、通知)
  • 测试隔离性差,状态泄漏

迁移到 Patrol

选择 Patrol 的原因:

  • 支持原生 OS 交互(权限、文件选择器、通知)
  • 更简洁的 API($('text')$(Icons.xxx)
  • 与 Flutter 测试框架无缝集成

配置 Android 支持

修改后的配置:

1
2
3
4
5
6
7
8

defaultConfig {
testInstrumentationRunner = "pl.leancode.patrol.PatrolJUnitRunner"
}

dependencies {
androidTestImplementation("androidx.test:runner:1.5.1")
}

创建 Android 测试入口:

1
2
3
4
5
6
7
8
9
10
11
12
13

@RunWith(Parameterized.class)
public class MainActivityTest {
@Parameters(name = "{0}")
public static Object[] testCases() {
PatrolJUnitRunner instrumentation =
(PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
instrumentation.setUp(MainActivityTest.class);
instrumentation.waitForPatrolAppService();
return instrumentation.listDartTests();
}

}

2. 根本原因分析

问题 1: Patrol CLI 版本不兼容

pubspec.yaml 指定 patrol: ^3.14.0,但全局安装的 patrol_cli 为 4.4.0,两者不兼容。

解决方案:

1
dart pub global activate patrol_cli 3.11.0

问题 2: Android 依赖版本冲突

Gradle 要求 androidx.test:runner:1.5.1,但代码中引入了 1.6.2。

解决方案:

1
androidTestImplementation("androidx.test:runner:1.5.1")

问题 3: 多个 patrolTest 导致 Isar 冲突

最初在一个模块文件中编写多个 patrolTest,每个测试都会调用 initializeTestApp(),导致 Isar 实例重复打开。

原因分析:

  • Patrol Android runner 将每个 patrolTest 作为独立的 instrumented test
  • 多个测试在同一进程中运行时,Isar 实例状态冲突
  • Patrol 的 PatrolAppServiceClient 在后续测试中返回 500 错误

解决方案:

  • 每个模块文件只保留一个 patrolTest,内部通过步骤分割测试场景
  • Isar 实例使用单例模式复用
1
2
3
4
5
6
7
8
9
10
11
12
Isar? _isarInstance;

Future<ProviderScope> initializeTestApp() async {
if (_isarInstance == null || !_isarInstance!.isOpen) {
_isarInstance = await Isar.open(...);
}

await _isarInstance!.writeTxn(() async {
await _isarInstance!.items.clear();

});
}

问题 4: 物品保存失败(缺少过期日期)

点击 “Save” 后无反应,测试卡在添加物品页面。

原因: 业务逻辑要求 expiryDate 必须设置,否则保存返回 false。

解决方案: 测试中添加过期日期选择步骤

1
2
3
4
5
await $(Icons.calendar_today_outlined).tap();
await $.pumpAndSettle();
await $('OK').tap();
await $.pumpAndSettle();
await $('Save').tap();

问题 5: 物品文本匹配失败(“too many”)

物品保存后,expect($('Test Apple'), findsOneWidget) 报错 “too many”。

原因: Dashboard 顶部有 “Expiring Soon” 横向卡片,物品同时出现在横滑卡片和主列表中。

解决方案:

1
2
expect($('Test Apple'), findsWidgets);  
await $('Test Apple').first.tap();

问题 6: 本地化文本不匹配

Settings 测试中 expect($('Data Overview'), findsOneWidget) 失败。

原因: 文本是本地化的,英文版本中 dataOverview = “Overview” 而非 “Data Overview”。

解决方案:

1
2
3

expect($('Overview'), findsOneWidget);
expect($('Backup & Restore'), findsOneWidget);

问题 7: GoRouter pressBack 导航问题

从 Settings 子页面(如 Categories)按返回键后,预期回到 Settings,但实际回到了 Dashboard。

原因: GoRouter 的路由栈管理,多次 pressBack 可能超出预期层级。

解决方案: 重新排序测试步骤,减少对 pressBack 的依赖,或每次子页面测试后重新导航到 Settings。


3. 解决方案

步骤一:添加 Patrol 依赖

1
2
3
4

dev_dependencies:
patrol: ^3.20.0
patrol_finders: ^2.4.1

步骤二:配置 Android

1
2
3
4
5
6
7

defaultConfig {
testInstrumentationRunner = "pl.leancode.patrol.PatrolJUnitRunner"
}
dependencies {
androidTestImplementation("androidx.test:runner:1.5.1")
}

步骤三:创建测试 Helper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Isar? _isarInstance;

final patrolTesterConfig = PatrolTesterConfig(
settleTimeout: const Duration(seconds: 10),
existsTimeout: const Duration(seconds: 10),
);

Future<ProviderScope> initializeTestApp() async {
if (_isarInstance == null || !_isarInstance!.isOpen) {
_isarInstance = await Isar.open(...);
}

await _isarInstance!.writeTxn(() async {
await _isarInstance!.items.clear();
await _isarInstance!.locations.clear();
await _isarInstance!.categorys.clear();
});
await DatabaseSeeder.seedAll(_isarInstance!);
return ProviderScope(...);
}

步骤四:编写模块化测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

void main() {
patrolTest('Dashboard - full module test', config: patrolTesterConfig, ($) async {
final app = await initializeTestApp();
await $.pumpWidgetAndSettle(app);


expect($('UseUp'), findsOneWidget);
expect($(Icons.add), findsOneWidget);


await $(Icons.search).tap();
await $.pumpAndSettle();
expect($(TextField), findsOneWidget);


await $(Icons.filter_list).tap();
await $.pumpAndSettle();
expect($('Category'), findsWidgets);
});
}

步骤五:更新 Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

ANDROID_HOME := $(HOME)/Library/Android/sdk
PATROL := ANDROID_HOME="$(ANDROID_HOME)" PATH="$(HOME)/.pub-cache/bin:/Users/mac/fvm/versions/stable/bin:$(ANDROID_HOME)/platform-tools:$(PATH)" patrol

.PHONY: e2e-smoke
e2e-smoke:
$(PATROL) test -d emulator-5554 -t integration_test/app_test.dart

.PHONY: e2e-android
e2e-android:
$(PATROL) test -d emulator-5554 -t integration_test/modules/dashboard_test.dart
$(PATROL) test -d emulator-5554 -t integration_test/modules/inventory_test.dart
$(PATROL) test -d emulator-5554 -t integration_test/modules/settings_test.dart
$(PATROL) test -d emulator-5554 -t integration_test/modules/history_test.dart

.PHONY: e2e-dashboard
e2e-dashboard:
$(PATROL) test -d emulator-5554 -t integration_test/modules/dashboard_test.dart

4. 预防与建议

  • 单测试单模块原则:每个模块文件只包含一个 patrolTest,通过步骤分割测试场景,避免 Isar 实例冲突。
  • Isar 单例复用:使用全局变量 _isarInstance 复用数据库实例,每次测试开始时清理数据而非重新打开。
  • 业务逻辑前置了解:编写测试前先了解业务规则(如物品必须有过期日期),避免因验证失败导致的测试卡住。
  • 本地化文本检查:测试中使用英文环境时,需确认英文翻译文本,避免因语言差异导致的断言失败。
  • UI 结构考虑:了解 UI 布局(如横滑卡片 + 主列表),使用 findsWidgets.first 处理多匹配场景。
  • 版本兼容性表:参考 Patrol 兼容性表 确保 patrol_cli 与 patrol 版本匹配。

5. 最终成果

测试覆盖

模块测试内容状态
Dashboard启动验证、搜索、筛选✅ Android / ✅ iOS
Inventory添加、验证、详情、消耗、删除✅ Android / ✅ iOS
Settings数据管理、分类、存储位置、语言✅ Android / ✅ iOS
History空状态、消耗后出现、补货✅ Android / ✅ iOS
Smoke跨模块快速验证✅ Android

iOS 排坑详情:参见 Patrol iOS 集成测试排坑实录

运行命令

1
2
3
4
5
6
7
make e2e-smoke       
make e2e-android
make e2e-ios
make e2e-dashboard
make e2e-inventory
make e2e-settings
make e2e-history

—.