Testing Legacy Strategies
SKILL.md
Testing Legacy Strategies (遗留代码测试)
Instructions
- 仅在缺乏测试的既有代码上使用
- 先填写 Required Inputs(高风险路径、测试栈、阻挡策略)
- 依照下方章节顺序创建安全网
- 一次只锁定一类行为或输出
- 完成后对照 Quick Checklist
When to Use
- Scenario C:旧项目现代化前的安全网
- Scenario F:共享逻辑需要测试保障
Example Prompts
- "请依照 Characterization Tests 章节,替这个类别创建现状测试"
- "用 Robolectric 章节,为依赖 Framework 的 Activity 写测试"
- "请用 Detekt/Lint Baseline 章节创建技术债控管"
Workflow
- 先确认 Required Inputs(风险路径、测试优先级、CI 门槛)
- 创建 Characterization Tests 锁定行为
- 补齐 Framework 测试与 MockK 策略
- 建立 Baseline 与 CI 门禁,避免技术债回流
- 执行 Legacy Test Gate 并用 Quick Checklist 验收
Practical Notes (2026)
- 测试输出必须可重复与可比对
- 每次只锁定一类行为,避免测试爆量
- Baseline 逐步收敛,避免一次性大改
- 先覆盖高风险业务,再扩展到普通路径
- flaky case 必须标记 owner 与修复截止时间
Environment & Compatibility (先确认)
- 在开始前先记录测试栈:
AGP/Kotlin/JUnit4 or JUnit5/Robolectric/MockK/kotlinx-coroutines-test - 同一模块避免混搭两套测试 runner;若必须混搭,先确认执行入口与依赖冲突
- Robolectric 版本需与项目
compileSdk与 AGP 组合先做 smoke test - 所有示例版本号以项目
libs.versions.toml或既有依赖锁定为准 - 若版本未定,请先完成 1 个最小可执行测试再批量铺开
Minimal Template
目标:
测试范围:
高风险路径:
CI 阻挡门槛:
行为锁定:
回归方式:
验收: Quick Checklist
Required Inputs (执行前输入)
高风险路径(金流/登录/权限/写入)测试栈版本(JUnit/MockK/Robolectric/coroutines-test)CI 阻挡策略(哪些失败阻挡合并)Baseline 策略(是否允许、收敛计划)责任人(测试 owner)
Deliverables (完成后交付物)
Characterization tests列表Framework 相关测试(Robolectric/Instrumented)Baseline文件与收敛计划CI 测试门禁配置Legacy Test Gate验收记录
Legacy Test Gate (验收门槛)
./gradlew test
./gradlew connectedDebugAndroidTest
PR 需要说明新增测试覆盖了哪些高风险行为。
Characterization Tests (现状测试)
为没有测试的旧代码撰写「现状测试」,不管对错,先锁定行为。
样本选择顺序 (避免盲目铺量)
- 先测高风险路径:金流、登录、权限、数据写入
- 再测边界条件:
null、空集合、最小/最大值、异常输入 - 最后补常见主路径:最常被调用的 20% 场景
- 每轮只新增一类行为,确保失败原因单一可诊断
策略
// 1. 先写一个会失败的测试
@Test
fun `calculateDiscount returns unknown value`() {
val result = legacyCalculator.calculateDiscount(100.0, "VIP")
assertEquals(0.0, result) // 故意用错误的预期值
}
// 2. 运行测试,记录实际回传值
// AssertionError: expected 0.0 but was 15.0
// 3. 更新测试为实际值
@Test
fun `calculateDiscount returns 15 percent for VIP`() {
val result = legacyCalculator.calculateDiscount(100.0, "VIP")
assertEquals(15.0, result) // 锁定现有行为
}
批量生成
@ParameterizedTest
@CsvSource(
"100.0, VIP, 15.0",
"100.0, REGULAR, 5.0",
"50.0, VIP, 7.5"
)
fun `calculateDiscount characterization`(
price: Double,
tier: String,
expected: Double
) {
assertEquals(expected, legacyCalculator.calculateDiscount(price, tier))
}
Robolectric (Android Framework 测试)
处理高度依赖 Android Framework 的旧单元测试。
设置
// build.gradle.kts
testImplementation("org.robolectric:robolectric:<project-verified-version>")
// 测试类别
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33])
class LegacyActivityTest {
private lateinit var activity: LegacyActivity
@Before
fun setUp() {
activity = Robolectric.buildActivity(LegacyActivity::class.java)
.setup()
.get()
}
@Test
fun `activity displays correct title`() {
assertEquals("Expected Title", activity.title)
}
}
Shadow 使用
@Test
fun `shows toast on error`() {
activity.showError("Network failed")
assertEquals("Network failed", ShadowToast.getTextOfLatestToast())
}
MockK for Kotlin
基本用法
@Test
fun `repository returns cached data`() {
val repository = mockk<UserRepository>()
every { repository.getUser("123") } returns User(id = "123", name = "Test")
val result = repository.getUser("123")
assertEquals("Test", result.name)
verify { repository.getUser("123") }
}
Coroutines Support
@Test
fun `suspend function mocking`() = runTest {
val api = mockk<UserApi>()
coEvery { api.fetchUser("123") } returns User(id = "123", name = "Test")
val result = api.fetchUser("123")
assertEquals("123", result.id)
assertEquals("Test", result.name)
coVerify(exactly = 1) { api.fetchUser("123") }
}
Dispatcher 控制 (降低 flaky)
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
Relaxed Mocks
// 自动回传默认值,适合旧代码测试
val service = mockk<LegacyService>(relaxed = true)
Detekt/Lint Baseline
渐进式收紧旧项目的品质标准。
生成 Baseline
# Detekt
./gradlew detektBaseline
# 产生 config/detekt/baseline.xml
# Lint
./gradlew lintDebug -Dlint.baselines.continue=true
# 产生 lint-baseline.xml
只检查新代码
// build.gradle.kts
android {
lint {
baseline = file("lint-baseline.xml")
}
}
detekt {
baseline = file("config/detekt/baseline.xml")
}
渐进式修复
# 每个 Sprint 减少 Baseline 中的项目
# 1. 修复一批问题
# 2. 重新生成 Baseline
./gradlew detektBaseline
CI 门禁 (防止回流)
# PR 常规检查:不允许新增违规
./gradlew detekt lintDebug
# 建议策略:
# 1. Baseline 文件只允许在「技术债收敛任务」中变更
# 2. 业务 PR 若修改 baseline,需额外说明与审批
Golden Master Testing
适合复杂输出 (HTML, JSON) 的旧代码。
@Test
fun `report generator produces expected output`() {
val output = legacyReportGenerator.generate(testData)
// 首次运行:保存为 golden file
// File("src/test/resources/golden/report.html").writeText(output)
// 后续运行:比对
val golden = File("src/test/resources/golden/report.html").readText()
assertEquals(golden, output)
}
实务建议:
- 先做标准化再比对(时间、时区、随机值、UUID、排序)
- Golden 文件纳入版本控制并在 PR 中审阅 diff
- 输出过大时改用结构化断言 + 关键片段 golden,降低维护成本
Quick Checklist
- Required Inputs 已填写并冻结(高风险路径/测试栈/门槛)
- 重构前先撰写 Characterization Tests
- 每次只锁定一类行为,失败原因可单点定位
- 使用 Robolectric 处理 Android 依赖
- MockK/Coroutine 测试同时验证「调用次数 + 业务结果」
- 使用
MainDispatcherRule或同等机制固定调度器 - Detekt/Lint Baseline 控制技术债
- CI 阻挡新增违规,不让 baseline 扩张
- 每个 Sprint 减少 Baseline 项目
- 固定时区、Locale、随机种子,确保测试可重复
- 对 flaky 测试标记原因与修复期限,不长期跳过
- Legacy Test Gate 已执行并记录结果