Testing Legacy Strategies
Installation
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 已执行并记录结果
Related skills
More from fwrite0920/android-skills
crash monitoring
Crashlytics 设置、ANR 分析与结构化日志
9deep performance tuning
Systrace, Memory Analysis, R8 优化与 App Startup 调校
9android skill index
资深 Android 工程师技能导航中心,根据场景推荐适合的技能组合
7coding style conventions
Kotlin 代码规范、Linter 配置与 Code Review 检核标准
7navigation patterns
Deep Links、跨模块导航与复杂 Back Stack 管理
7devops and security
CI/CD 自动化、Gradle 优化与应用程序安全加固
7