目标 :保证 Markdown 模块 正确、快速、稳定 。
范围:
- 单元 / UI / 快照 / 端到端测试
- 渲染埋点与性能监控
CI/CD 与灰度 属于 低优先可选 。如需集成,请参考本章末尾的 附录 A。
6.1 测试金字塔
| 层次 | 覆盖内容 | 工具 / 框架 |
|---|---|---|
| 单元 (Unit) | Markdown 解析、工具函数 | JUnit5 · Truth |
| UI (Widget) | View 属性、交互 | Espresso · Robolectric |
| 快照 (Snapshot) | 像素级一致性 | Paparazzi · Shot |
| 端到端 (E2E) | 实机网络、路由 | AndroidX Test · MockWebServer |
6.2 单元测试示例
kotlin
@Test
fun heading_shouldParseAsHeadingNode() {
val node = Parser.builder().build().parse("# Title")
assertThat(node.firstChild).isInstanceOf(org.commonmark.node.Heading::class.java)
}
- 每条 CommonMark 语法 + 自定义插件各写 1 成功 + 1 失败用例。
- 渲染前后
RenderProps行为也需断言。
6.3 UI & 快照测试
6.3.1 Espresso
kotlin
@Test
fun notePanel_toggleBody() {
launchActivity<DemoActivity>().use {
onView(withText("NOTE")).perform(click())
onView(withId(R.id.body)).check(matches(isDisplayed()))
}
}
6.3.2 Paparazzi 快照
kotlin
@Test
fun codeBlock_snapshot() {
paparazzi.snapshot {
CopyableCodeBlockView(context).apply {
render(context, SAMPLE_CODE, defaultParams, null)
}
}
}
- 快照 diff > 1 % 视为渲染回归,CI 自动 fail。
6.4 渲染埋点与监控
6.4.1 关键指标
| 指标 | 目标 (p95) | 说明 |
|---|---|---|
| Parse_TT (ms) | ≤ 50 | Parser.parse() |
| Render_TT (ms) | ≤ 90 | Markwon.setMarkdown() |
| FCP (ms) | ≤ 500 | Activity 首帧 |
| Jank_% | ≤ 5 % | 滚动掉帧率 |
| Image_Fail (%) | ≤ 1 % | 图片加载失败率 |
6.4.2 Markwon 插件埋点
kotlin
class MetricsPlugin(private val log: (String, Long) -> Unit) : AbstractMarkwonPlugin() {
private var t0 = 0L; private var tParse = 0L
override fun beforeSetText(tv: TextView, md: String) { t0 = now() }
override fun configureParser(b: Parser.Builder) {
b.postProcessor { node -> log("Parse_TT", now() - t0); node }
}
override fun afterRender(node: Node) { tParse = now() }
override fun afterSetText(tv: TextView) { log("Render_TT", now() - tParse) }
private fun now() = SystemClock.elapsedRealtimeNanos()
}
6.4.3 帧率采集
kotlin
val agg = FrameMetricsAggregator.EVERY_DURATION
agg.add(activity.window)
// onStop
val slow = agg.frameMetrics[0]?.get(FrameMetrics.TOTAL_DURATION) ?: 0
6.4.4 图片监控 (Glide)
kotlin
glide.load(url)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, m: Any?, t: Target<Drawable>?, p: Boolean): Boolean {
Metrics.log("Image_Fail", 1); return false
}
override fun onResourceReady(r: Drawable?, m: Any?, t: Target<Drawable>?, d: DataSource?, f: Boolean): Boolean {
Metrics.log("Image_OK", 1); return false
}
}).into(iv)
6.4.5 数据上报
proto
message MdMetric {
enum Type { PARSE_TT = 0; RENDER_TT = 1; FCP = 2; JANK = 3; IMAGE_FAIL = 4; }
required Type type = 1;
required int64 value = 2; // ms 或 次数
optional string ver = 3; // App 版本
}
- 优先本地批量 + gzip,再 HTTPS 上报。
- Grafana 面板:Parse TT、Render TT、Jank_% 随版本折线。
6.5 快照 & 差异治理流程
- PR 中的 GitHub Action 运行
./gradlew verifyPaparazziDebug - 差异>1 % → Action fail,输出 diff PNG
- 开发核对:
- UI 设计变更 → 更新快照生成新的 baseline
- 非预期 → 修正代码,再跑一次 CI
6.6 故障排查指南
| 现象 | 快速定位 | 可能根因 | 解决 |
|---|---|---|---|
| 首帧 > 1 s | Trace 查看 Parser.parse |
大文档同步解析 | 后台线程 + LRU 缓存 |
| 滚动掉帧 | FrameMetrics 看 Draw > 16 ms | 复杂表格反复 measure | View 复用 / 分页 |
| 图片空白 | Image_Fail% 升高 | URL 失效 / SSL | 占位图 + 重试 |
| 内存涨 | LeakCanary Trace | Span 持有 Context | 使用 applicationContext |
6.7 章节 Checklist
| 阶段 | 动作 |
|---|---|
| 测试 | 单元 + UI + 快照 覆盖率>80 % |
| 埋点 | Parse/Render/FCP/Jank/ImageFail |
| 监控 | Grafana 仪表板实时可见 |
| 故障 | 快照 diff + LeakCanary 自动告警 |
附录 A(可选):CI/CD 与灰度发布
优先级低------可在基础功能稳定后按需接入
- GitHub Actions / GitLab CI
- 步骤:依赖缓存 → 单测 & 快照 → Lint → Assemble → 上传 APK
- Gradle 多 flavor
canary / beta / prod打包脚本BuildConfig.BOOL_PLUGIN_ENABLED控制插件灰度
- 远程开关
- Firebase Remote Config / 自建 CDN JSON
- 指标监控低于阈值可热关新插件
- fastlane
- 一键推送到内测 & 生产
- 自动回滚脚本:CrashFree < 99 % → 压回旧版本
若需完整 YAML / fastlane 脚本,可参考上一版本内容或内部 DevOps 模板。
小结
- 测试金字塔 + 快照------锁死回归
- 埋点监控------发现性能衰退
- CI/CD------可选加速手段,后期接入
- 灰度------版本健康,数据驱动开关新能力