Pangolin 统一测试框架 · 架构说明

分层方法论 + 脚手架模板 + 单一编排入口 · 可自动化 / 可量化 / 可自定义

定性(方案 A):不存在一个能同时覆盖「界面渲染 / 交互驱动 / 后端记账 / 真链路端到端」的单一工具——这四者运行时根本不同。所以「一套测试框架」= 一套分层方法论 + 一组可复用脚手架模板(假桥、Provider 覆盖、golden+字体锁、Go 真库套、E2E compose 骨架)+ 一个统一编排入口。跨多端项目的复用方式是模板复制后按接缝适配,而非全项目 import 同一个包。
↔ 配套文档:开发规范 · 可测试性五支柱(「怎么写才好测」——测试能力的前置条件)

设计原则

前不久的「mac 实时统计恒为 0」就是典型反例:原生侧产出统计、Dart 侧消费统计,两端的 pangolin/vpn/stats 契约没有任何自动化测试守门,触发条件一变(隧道已连接才启动 app)就静默失效。L0 契约层防的正是这类 bug。

① 分层测试金字塔

图 1 · 五层金字塔:L0 契约为地基,L1→L4 自底向上

L4 E2E L3 视觉一致性 golden · pixelmatch L2 集成 · 交互 flutter flow · httptest · bufconn · 真库 L1 单元 flutter_test · go test+testify(纯逻辑) L0 契约 / 静态闸 · 地基 契约快照 · OpenAPI 校验 · analyze/vet · 红线词扫描 L4 E2E docker-compose 真链路冒烟 越往上:越真实·越慢·越脆·越少 越往下:越多·越快·越稳·越省

② 契约接缝数据流

统计/记账数据沿一条跨进程链路流动。每段接缝标注其契约,以及哪一层测试在此下刀——这张图就是「该测什么、在哪测」的地图。

图 2 · 契约接缝:从节点数据面一路回到客户端 UI

原生隧道 / libbox sing-box(客户端) Flutter Dart 层 UI / Riverpod Go 控制面 :8080 HTTP + DB agent :9443 gRPC mTLS 节点数据面 sing-box 出网 ① stats EventChannel ② /v1/* HTTP JSON ③ UsageEntry gRPC ④ V2Ray stats L0 契约 + L2 flow VpnBridgeMock L2 httptest ApiClient 注入 L2 bufconn ✓强 自签 CA/CRL L4 真流量 ✗缺 E2E 待建

实时上下行走 ①(libbox→EventChannel→Dart);累计流量/周柱走 ④→③→DB→②(节点统计→agent→控制面入库→客户端拉取)。两条流共用同一张接缝图,测试各自归位。

③ 四类诉求 → 层 → 工具 → 量化指标

把你提出的四类测试诉求逐条落到层、工具、量化指标与现状:

诉求工具量化指标现状
① 界面设计一致
每端一致
L3 Flutter golden(字体锁定,host 无关→天然每端一致);design-distill pixelmatch 对照 HTML 原型真相源 像素不一致比例 vs 阈值 0.05;golden 通过/失败 素材齐 · CI 没跑
② 前端交互
连接 / 登陆 / 登出
L2 flutter_test 进程内驱动整树 + VpnBridgeMock + http MockClient + ProviderScope 覆盖 流程通过/失败;覆盖率 lcov 素材齐 · 待组装
③ 数值型
统计 / 实时速度 / 时延
L1 + L4 L1:格式化/换算纯逻辑单测;L4:连真节点拉 stats 断言 bytes 增长、延迟 >0 实测 vs 期望容差;时延 ms;速率 B/s >0 L1 可补 L4 缺
④ 后端网络
按租户流量 / 限流
L2 + L4 L2:SQLite 真库 + testcontainers MySQL(已覆盖并发/重放/跨日/配额);L4:全链路真流量计数 记账数值断言;限流是否触发;覆盖率 go cover L2 强 L4 缺

④ 逐层详解

L0 · 契约 / 静态闸

守住接缝的「字段形状」

L1 · 单元

纯逻辑,最快最稳

L2 · 集成 / 交互

带假替身的真实路径

L3 · 视觉一致性

每端渲染一致 + 对照设计真相源

L4 · 端到端冒烟

真链路:统计真不真 / 流量真不真

⑤ 编排与 CI 流水线

一个入口按 profile 分流:CI 每 push 只跑快而稳的 fast;视觉与 E2E 按需触发。

图 4 · 单一入口 → 三 profile 泳道

单一入口 make test / test.sh fast L0 + L1 + L2(假桥/真库) 触发:每次 push(nas CI) 产物:通过/失败 + lcov 覆盖率 visual L3 golden + pixelmatch 触发:push 或本地 产物:pixel diff 报告(HTML) e2e L4 真链路冒烟 触发:本地 / 按需(docker±Mac) 产物:数值容差报告

⑥ L4 端到端冒烟拓扑

图 5 · docker-compose 真链路 + 测试驱动器(推荐边界 a)

测试驱动器 ① 登陆 /v1/auth/login ② 发起连接(真凭证) ③ 发真流量(curl/iperf) ④ 拉 /v1/usage ⑤ 断言 bytes↑ / 限流 docker-compose · 一台 Linux runner 控制面 :8080 HTTP + gRPC :9443 SQLite + Redis 记账存储 agent mTLS enroll · 上报 sing-box 数据面出网 回环出口 httpbin/echo ① ② ④ ③ 真流量经数据面出网 → 计数

⑦ 量化指标总表

维度指标工具 / 口径
覆盖率行/分支 %flutter test --coverage + go test -coverprofile → 合并 lcov
视觉不一致像素比例 vs 阈值 0.05pixelmatch(diff.mjs);golden 通过/失败
数值精度实测 vs 期望容差L4 断言 bytes/速率/累计落在容差带内
时延毫秒 msurltest 延迟 >0 且在合理区间
稳定性通过率 / flaky 率多次重跑统计;flaky 用例隔离

⑧ 可复用与可自定义

跨项目模板(复制即用,按接缝微调)

每项目按接缝定制:契约字段、API 路径、compose 服务清单、golden 屏清单、数值容差阈值。

⑨ 落地路线(建议)

动作性价比起点
1L0 契约快照 + 把现有 golden 接进 CI最高(防回归)素材已有,组装即可
2组装 L2 前端 flow 测试(登陆/续登/连接/登出)Mock/覆盖素材全齐
3L4 后端全链路冒烟 (a)中(覆盖"真不真")需新建 compose + 驱动器
4补 L1 数值单测 + L2 HTTP 鉴权测试增量补齐
5(b) desktop 真连 / (c) 真机——本地按需低(贵·脆)需常驻 Mac / 真机

本文档仅为架构说明(设计真相源)。具体落地拆解(契约快照实现、前端 flow 用例、L4 冒烟脚本)走各自实现计划(writing-plans)。

⑩ 已知缺口 / 测试盲区

框架不是「测了什么」的清单,更要诚实地记「没测什么、为什么、何时补」——否则盲区会散在对话与脑子里被遗忘。本节是这些缺口的单一跟踪源,补上一项就划掉一项。

A. 覆盖盲区(功能上没被验证到的部分)

缺口严重度现状 & 为什么没测何时 / 怎么补
前端统计「上屏数值对不对」 中·诉求半覆盖 最初诉求是「统计数据对不对」。后端记账已 L4 e2e(ReportUsage → /v1/usage 断言字节);前端只到单测(device_usage_test/format_test 解析+格式化)。解析对 ≠ 渲染对——没有测试验证统计页拿到真实响应后上屏的数字/曲线是对的。 可立即补:widget 级测试,用 e2e server 的真实 /v1/usage 响应喂 stats_page,断言关键数值/曲线上屏。闭合诉求最后一环。
真实流量数据路径 高·最大盲区 L4 e2e 是注入ReportUsage,不是「真 sing-box 出网 → v2ray stats 采集 → 上报记账」。真链路从未自动跑过——这是「我们其实不知道线上准不准」的根盲区。真出网那环仅 usage_v2ray.go 单测覆盖采集解析。 贵且脆(单机 docker 真出网重、CI 跑不动)。留后续按需,或本地手动跑一次真连基准。
原生隧道层 真正承载流量的代码——client/ios/Runner/VpnManager.swiftclient/android/…/PangolinVpnService.kt、macOS 系统扩展——零自动化测试,全靠真机/模拟器手测。 依赖真机/模拟器,难纯 CI 化。至少补「连上→可达性探针→断开」的半自动脚本。
契约双份可能各自漂移 Dart 与 Go 各冻一份字段面快照,两边是各自独立的真相,理论上可各自漂移(OpenAPI 改了、只更了一边)。 真·单源(从 OpenAPI 生成两端契约)是大改,列入 C 类效率项后续做。

B. 运维 / 工程缺口(能跑,但脆或欠账)

缺口严重度现状 & 为什么何时 / 怎么补
CI 是单点 中·运维 整条 CI 吊在一台 mac(mac-pangolin-2)+ 依赖 jiu 的 relay + Docker Desktop。launchd 持久化尚未激活(见 docs/ci-runner.md 的一次性 ! 命令)——mac 一关/重启 CI 就全停。 激活持久化(脚本已就位);理想归宿是 NAS Linux host runner。
go-integration job 脆性 跑在裸宿主机(go1.26.1,非容器,testcontainers 要真 docker,DooD 在 Docker Desktop mac 网络不通)——宿主 go 版本漂移即挂;套件 ~5min;曾踩 ryuk 关闭→容器泄漏坑(已用默认 ryuk + -p 1 串行规避)。 迁 NAS Linux host runner 可恢复容器化 hermetic;保持 ryuk 开启。
analyze 严格化(闸·B1) 中·阻塞 去掉 --no-fatal-infos 让 info 变致命,会因 HEAD stats_page.dartwithOpacity info 当场红;而清 info 正动 stats-overhaul 在改的同一文件。 被 stats-overhaul 耦合阻塞,其合并后立刻做。
golden 全集(闸·B2) 中·阻塞 golden job 现只跑 components + auth;tablet/desktop-stats golden 与 tablet_pages_golden_test.dart 全是 dirty WIP,并入等于钉死未定稿基线。 stats-overhaul 合并后并入;连带补回临时摘除的 DeviceUsage 契约快照(Dart+Go)+ e2e /v1/usage/devices 断言。
golden 本地难重生 低·摩擦 Linux 权威基线(mac 渲染不一致),开发机改 UI 后须 docker 起 flutter 容器重生基线,非纯本地。 scripts/update-goldens.sh 已封装,文档化即可,非真缺口。

维护约定:补上某项缺口后,从本表删去对应行(或标 已补);新发现的盲区即时登记到此,使本节始终等于「当前真实缺口」。