-
Notifications
You must be signed in to change notification settings - Fork 8
Testing
一句话 TL;DR:这套测试体系用 JUnit 把 340 所学校的解析契约、几何引擎、撤回快照、组件 manifest 接线和导入往返锁成可执行的回归网。本页给想加新学校、加新组件变体、改动几何/撤回的贡献者看。
本文基于 v1.0.54。
仓库的测试代码全部在 app/src/test/ 下,跑 ./gradlew :app:testDebugUnitTest 即可,不依赖设备。
测试类 166 个,用例 1499 个(@Test 数累加)。包结构按职责拆:
| 包 | 测试类数 | 锁的对象 |
|---|---|---|
data/jw |
43 | 教务解析器(Jw*Parser)+ 协议识别 + 340 校回归 + schools.json 一致性 |
data/parser |
19 | 文本/ICS/JSON/WakeUp 分享 + sleepy-v1 原生格式的导入导出 + 无损往返 |
util |
22 | 几何引擎(ConflictLayoutEngine、ClusterOwnTimeGeometry)、TimeTableUtils、HolidayManager、DateUtils
|
widget |
23 | 10 个 widget 变体的 manifest/接线/路由/渲染契约,跨厂商 pin 路由 |
ui/component |
2 | 冲突卡片几何、字体缩放 |
ui/screen |
16 | 编辑/导入/课表页的结构契约,WebView JS 与 Kotlin 双侧协议一致 |
ui/theme |
2 | 自定义主题派生、UI 契约 |
data/undo |
2 |
UndoManager 单槽语义、撤回快照覆盖率 |
data/entity |
3 | 实体、smartPeriod 配置、ICS 节次拆分 |
data/diff |
1 | 冲突行差分 |
data 根 |
3 | Room 迁移、实体标志、自定义主题核心 |
| 仓库根 | 6 |
StringsKeyParityTest、ManifestExternalOpenFilterTest、AppPrefsIsolationTest、BackRestoreSaveableContractTest、AboutLicenseAttributionTest、CourseColorUtilTest
|
测试运行走 app/build.gradle.kts:62 的 testOptions.unitTests,isIncludeAndroidResources = true + isReturnDefaultValues = true(app/build.gradle.kts:64-65)。Robolectric 全程未引入(grep 全测试树零命中),returnDefaultValues 模式让 Bitmap.createBitmap 之类返回 null 桩,纯 JVM 跑得动 widget 相关逻辑;代价是任何依赖真实位图/Context 的行为没法直接断言,改 widget 渲染时要么补源扫描契约测试,要么走真机验证。instrumented test 目录(app/src/androidTest/)不存在,依赖声明里的 androidTestImplementation 是默认模板遗留。
Room 实例、Compose 树、Glance 渲染都没法在纯 JVM 里搭起来。仓库的选择是不引 Robolectric,把「无法运行时构造的东西」改成「可静态读取的东西」来断言。三类策略撑住整个回归网:
第一类:源码级扫描测试。声明式数据和接口接线读源文件等价于读打包产物。一组测试直接 File("app/src/main/...").readText() 后用正则或字符串包含断言:
-
UndoCaptureCoverageTest(data/undo/UndoCaptureCoverageTest.kt:18)扫ScheduleRepository.kt,断言 14 个公开写方法体里都出现captureForUndo()(:45),并单独钉死applyDiff必须在第一次 DAO 写之前 capture(:54)。缺一个 capture,对应动作的撤回按钮就消失一次——测试注释里写明了这个后果。 -
WidgetInfoXmlContractTest(widget/WidgetInfoXmlContractTest.kt:21)用 DOM 解析AndroidManifest.xml+res/xml/*_widget_info.xml,断言 10 个 receiver ↔ 10 个 info XML 一一对应(:76、:124)、任何 info XML 不得声明android:configure(:98)、全部必须声明reconfigurablewidgetFeatures(:112)。这条契约阻止「添加桌面组件时弹配置白屏」。 -
StringsKeyParityTest(根目录,StringsKeyParityTest.kt:24)扫描 6 个 locale 目录的strings.xml,断言新加的 string key 在所有语言下都齐(:103)。缺任一即 MissingTranslation lint 回归。 -
ManifestExternalOpenFilterTest(根目录)读AndroidManifest.xml断言intent-filter注册面覆盖.html/.ics/.csv等 MIME,文件管理器点文件才能把内容送进导入入口。 -
AppPrefsIsolationTest(根目录)在字符串字面量级别断言两个 colorless key 互不相同、getter/setter 不互读。 -
ScheduleViewModeSessionContractTest(ui/screen/schedule/ScheduleViewModeSessionContractTest.kt:28)扫ScheduleScreen.kt+MainActivity.kt,钉死 viewMode 状态提升与会话层真相源,防「网格视图被弹回周视图」的回潮。
这类测试共 30 个(grep src/main 数到),覆盖 manifest、res xml、Kotlin 源、assets 四类声明式数据。实现套路一致:File("app/src/main/...") 与 File("src/main/...") 双路径试探兼容两种工作目录,读全文,再 Regex.containsMatchIn 或 assertTrue(String in text) 断言。断言消息里写清违约后果,红的时候贡献者不用翻历史就能知道该补什么。
第二类:协议 fixture 矩阵。每种教务协议都有真实采集的 HTML/JSON 样本和同名 *.expected.json oracle,parser 测试把两者一比对就能定位漂移。样本本身也是契约:JwProtocolFixtureMatrixTest 读 expected 文件里的 _fingerprint 五字段(urlMarkers / htmlMarkers / expectedProtocol / expectedConfidence / expectedParseResult),断言识别层对每个样本返回正确协议族,样本删除会触发下限闸。新协议想进识别层,加一份 html + expected 即被自动纳入。
第三类:行为引擎测试 + 无损往返。冲突布局、几何聚簇、解析往返、DB 迁移这些带真实逻辑但能在纯 JVM 跑的对象,走完整业务语义测试。
三类里的代表性测试类与各自锁的契约:
| 测试类 | 锁什么 |
|---|---|
UndoCaptureCoverageTest |
14 个公开写方法每个都先 capture 再写库 |
WidgetInfoXmlContractTest |
receiver ↔ info XML 一一对应,不弹配置页,保留 reconfigurable |
StringsKeyParityTest |
新 string key 在全部 6 个 locale 都存在 |
ManifestExternalOpenFilterTest |
外部打开 intent-filter 覆盖 .html/.ics/.csv 等 MIME |
ScheduleViewModeSessionContractTest |
课表视图模式状态提升到会话层,overlay 往返不丢 |
JwQzAppWebViewContractTest |
qz_app 抓取走专用 JS 分支,协议字段解码只在 Kotlin 侧 |
JwParserFixtureTest |
63 个脱敏样本九字段契约,顺序敏感 |
JwProtocolFixtureMatrixTest |
expectedProtocol 指纹与 detectProtocolFromHtml 一致 |
SchoolsJsonConsistencyTest |
schools.json 的 type 可声明、可路由、displayName/category 不缺 |
Schools179CrossValidationTest |
条目总数 340,私网地址与死链永不入清单 |
ConflictLayoutEngineTest |
聚簇闭包、变体分配、z 序翻转 |
ClusterOwnTimeGeometryTest |
簇内几何 (startFrac, endFrac) 契约与坐标系一致 |
PinWidgetRoutingTest |
widget_type 路由覆盖全部 10 个变体,未知值回落 WeekGrid |
CourseAliasMigrationTest |
Room 迁移 SQL 单一事实来源,版本链严格连续 |
第二类四个核心类的细节:
-
JwParserFixtureTest(data/jw/JwParserFixtureTest.kt:18)63 个手写脱敏样本走九字段契约(类头注释 :11),每个样本的name/day/startNode/endNode/startWeek/endWeek/type/teacher/room与*.expected.json逐字对比(:110)。case 表注册量有下限闸(:104 钉至少 60 条),防样本被悄悄删。 -
JwProtocolFixtureMatrixTest(data/jw/JwProtocolFixtureMatrixTest.kt:14)读detection-pages/*.expected.json的_fingerprint字段,断言detectProtocolFromHtml对每个样本返回正确的协议族(:17),并要求htmlMarkers至少一条命中。断言数本身有下限(至少 10 个正样本),缺一 = 有协议族的 expected.json 被删。 -
JwProtocolDetectionTest(data/jw/JwProtocolDetectionTest.kt)用@RunWith(Parameterized)把 URL/HTML 样本参数化,逐个钉死detectProtocolFromUrl与detectProtocolFromHtml的返回值,对抗样本(webvpn URL、指纹混淆页)也在参数表里。 -
Schools179CrossValidationTest(data/jw/Schools179CrossValidationTest.kt:19)钉死schools.json总数 340(:179)、RFC1918 私网地址永不入清单(:45)、已出售/废弃域永不再出现。名字里的 179 是历史基线,现在已涨到 340,新增学校必须同步抬这个数。
第三类挑几个有代表性的:
-
IrregularOverflowTest(util/IrregularOverflowTest.kt:20)钉死非常规时间课跨午间空隙不被吸附到下午节次——12:30 结束的课不许与 14:00 的课判冲突。 -
ExportImportRoundTripTest/ExportNodesRoundTripTest/ParseNodesLosslessTest(data/parser/)锁死 13 节课表(课程真到 13 节)经过 shareText / JSON / ICS / sleepy-v1 各格式 round-trip 后nodesPerDay仍是 13,timeJson不缩水。设计目标一句话:导入导出都必须无损,「没时间块」不等于「没节次」。 -
WakeUpShareNodesLosslessTest(data/parser/WakeUpShareNodesLosslessTest.kt:13)用一份真实 WakeUp 分享文本(8 门实验课,startNode=11 step=3)锁住课程到达 13 节时不被老表的 10 节声明钳住。 -
Ics28NeuPeriodsTest(data/parser/Ics28NeuPeriodsTest.kt)用neu-schedule-2026-2027-1.ics(app/src/test/resources/ 下)钉死 NEU 五教学块到节次的映射与连排事件拆分。 -
CourseAliasMigrationTest(data/CourseAliasMigrationTest.kt:20)走 sqlite-jdbc 直连内存库,跑 Room 迁移同一份MIGRATION_5_6_STATEMENTS,断言 v5 → v6 列加得对、旧行 alias 默认 ''、ALL_MIGRATIONS链从 3 起步严格连续(防漏登中间版本)。
两组根目录,各管一摊:
老根 app/src/test/resources/jw_fixtures/(488 文件,git 跟踪)。按协议族分子目录:detection-pages(协议判型样本 + 指纹 oracle)、adversarial(10 个子目录:cross-fp-parsers、empty-semester、encoding-gbk、fp-url-confusion、iframe-nested、image-table、json-boundaries、login-expired、merged-rows、webvpn-urls,共 291 文件,专喂协议识别和解析器的对抗样本)、zf-old-table1、zf-old-variants、zf-new-html、zf-new-kblist、qz-old、qz-base-crazy、qz-br-withnode、wisedu-json、cf-chengfang、hnust-urp、pku-bnuz。detection-pages 顶部的 README.json 写明每个协议的样本清单与断言字段。JwParserFixtureTest 消费其中的 63 个手写脱敏样本,oracle 是同名 *.expected.json 的 courses 数组(九字段,顺序敏感,禁止自动重排)。
新根 app/src/test/resources/jw/fixtures/(16 个协议子目录 + adversarial,共 73 文件)。按学校分目录:neu、ucas(7)、qz_app、boya_pp、bjtu、jou、scu、seu、ustc、zju、chaoxing、eams5(6)、qz_br(3)、qz_ieas、qz_old(4)、qz_with_node(3)。命名沿用「学校或协议 slug + 时间戳」,如 neu/courses.sample.json、ucas/real-detail-0908/(真实采集包分片)、ucas/person-schedule.real-0906.html、boya_pp/ysu-2026-2027-1.json。解析器走 javaClass.classLoader!!.getResourceAsStream("jw/fixtures/<dir>/<file>") 加载。
加新协议 fixture 的标准做法(参考 data/jw/JwNewZfParserTest.kt:24 的注释,该类加载 zf-new/ 顶层的 kblist/html 样本):
- 从采集包裁一段真实数据,姓名学号打码,存进
jw/fixtures/<你的协议>/。 - 同名写一份解析后的 oracle(走
JwParserFixtureTest风格就写*.expected.json九字段数组;走单 parser 测试就写*.sample.json)。 - 在对应
Jw*ParserTest里加加载方法与断言;若走JwParserFixtureTest风格,在它的 case 表登记一行。 - 新协议要进识别层,在
detection-pages/下加一份*.html与同名*.expected.json(_fingerprint里写urlMarkers/htmlMarkers/expectedProtocol/expectedConfidence/expectedParseResult五个字段),JwProtocolFixtureMatrixTest与JwProtocolDetectionTest自动纳入。 - 跨语言场景(JS 注入侧 + Kotlin 解析侧都要解码同一协议字段)再加一个
*WebViewContractTest,扫 JS 源码断言字段名只在 Kotlin 侧出现。
主资产同步:schools.json 的测试副本 app/src/test/resources/jw/schools.json 必须与 app/src/main/assets/schools.json 1:1。JwNewSchoolsTest 加载副本失败时直接抛错并给出 cp 命令(app/src/test/java/com/lingion/sleepy/data/jw/JwNewSchoolsTest.kt:20),照提示复制即可。
仓库根目录执行:
./gradlew :app:testDebugUnitTest跑单个测试类:
./gradlew :app:testDebugUnitTest --tests "com.lingion.sleepy.data.jw.JwParserFixtureTest"跑单个用例:
./gradlew :app:testDebugUnitTest --tests "*UndoCaptureCoverageTest.every public write method captures undo snapshot"无 Android SDK 也跑得动:这是纯 JVM 测试,只用 junit:4.13.2、org.junit.jupiter:junit-jupiter-params:5.10.2、org.json:json:20231013、org.xerial:sqlite-jdbc:3.53.4.0(app/build.gradle.kts:146-151)。
契约测试(*ContractTest、*CoverageTest、*MatrixTest 这一类)的红色信号有几类,处理原则:
fixture 漂移:expected.json / *.sample.json 与 parser 输出对不上。分两路:JwParserFixtureTest 的九字段 oracle 方向固定——字段不符修 parser,禁止改 expected 迎合现状(类头注释原话);单校 parser 测试(如 JwNeuParserTest、JwUcasParserTest)的 fixture 是采集包脱敏件,协议行为确认变化时改 fixture 并在注释写明依据。
源扫描契约红:UndoCaptureCoverageTest、WidgetInfoXmlContractTest、ScheduleViewModeSessionContractTest 这一类几乎一定意味着新加的方法/字段没接进流程。先读测试 KDoc 注释找契约意图,再对照 app/src/main/ 的对应源码。断言消息通常已写明违约后果,照着补接线。
几何/解析引擎红:先确认是哪个几何真值函数被动了——渲染与单测共用一份函数,红通常意味着有人只改了其中一条调用路径。用最小数据复现,先红再绿。
DB 迁移红:CourseAliasMigrationTest 红通常意味着 ALL_MIGRATIONS 漏登中间版本或顺序错乱,「从 3 起步严格连续」的断言会直接点名缺哪一版。
总原则:红色不能靠改测试让它绿。改了行为,必须同步动对应的测试;教务解析必须先写一个失败的测试再改代码让它通过(红→绿),每所学校带 fixture 测试;WebView 内 JS 与 Kotlin 两侧的协议解码必须同步通过对应的 *WebViewContractTest,两边写得不一致,页面表现和导入结果就对不上。提 PR 时勾掉 .github/PULL_REQUEST_TEMPLATE.md 里的两条 checklist:testDebugUnitTest 全绿、lintDebug 无新增错误。
源码:
app/src/test/java/com/lingion/sleepy/(166 个测试类)、app/src/test/resources/jw_fixtures/(488 文件,老根)、app/src/test/resources/jw/fixtures/(73 文件,新根)、app/src/test/resources/zf-new/、app/build.gradle.kts:62-65(testOptions)、app/build.gradle.kts:146-151(testImplementation)、.github/PULL_REQUEST_TEMPLATE.md(testDebugUnitTest + lintDebug 双闸)。
Sleepy Wiki
入门
界面与视图
课程管理
导入导出
小组件与提醒
数据与设置
项目
社区