HarmonyOS开发:回归测试——版本回归测试
HarmonyOS开发:回归测试——版本回归测试
📌 核心要点:改了A模块,B模块挂了——这种"改一个坏一个"的回归Bug是开发者的噩梦。回归测试就是每次代码变更后自动验证"改了的东西该改,没改的东西没坏",用自动化套件守护每一个版本的质量底线。
一、背景与动机
你有没有经历过这种场景?修了一个Bug,结果引入了两个新Bug。优化了启动速度,结果页面加载变慢了。升级了第三方库,结果某个功能直接崩了。每次发版都像拆盲盒——不知道哪个功能会出问题。
回归Bug有个特点——你永远不知道它会在哪里出现。你改了用户模块的代码,订单模块可能就挂了,因为订单模块依赖用户模块返回的数据格式。你优化了数据库查询,缓存模块可能就失效了,因为查询结果的结构变了。这种"牵一发而动全身"的问题,手动验证根本覆盖不过来。
回归测试解决的就是这个问题:每次代码变更后,自动跑一遍所有关键测试,确保没有引入回归Bug。不是"手动验证一下改的功能",而是"自动验证所有功能"。自动化回归测试套件是CI/CD流水线的核心组件——没有它,你就不敢放心地合并代码。
二、核心原理
2.1 回归测试体系
flowchart TB
A[回归测试体系] --> B[回归测试策略]
A --> C[自动化测试套件]
A --> D[版本兼容性验证]
A --> E[效率优化]
B --> B1[全量回归]
B --> B2[选择性回归]
B --> B3[增量回归]
B --> B4[风险驱动回归]
C --> C1[冒烟测试套件<br/>核心功能验证]
C --> C2[功能测试套件<br/>全面功能覆盖]
C --> C3[集成测试套件<br/>模块间协作]
C --> C4[端到端测试套件<br/>用户场景验证]
D --> D1[API兼容性]
D --> D2[数据兼容性]
D --> D3[配置兼容性]
D --> D4[平台兼容性]
E --> E1[测试用例优先级]
E --> E2[并行执行]
E --> E3[增量测试]
E --> E4[失败用例分析]
classDef mainStyle fill:#4CAF50,stroke:#388E3C,color:#fff,font-weight:bold
classDef strategyStyle fill:#3498DB,stroke:#2980B9,color:#fff
classDef suiteStyle fill:#2ECC71,stroke:#27AE60,color:#fff
classDef compatStyle fill:#F39C12,stroke:#E67E22,color:#fff
classDef effStyle fill:#9B59B6,stroke:#8E44AD,color:#fff
class A mainStyle
class B,B1,B2,B3,B4 strategyStyle
class C,C1,C2,C3,C4 suiteStyle
class D,D1,D2,D3,D4 compatStyle
class E,E1,E2,E3,E4 effStyle
2.2 回归测试策略对比
| 策略 | 执行范围 | 执行时间 | 覆盖率 | 适用场景 |
|---|---|---|---|---|
| 全量回归 | 所有测试用例 | 长 | 100% | 大版本发布 |
| 选择性回归 | 受影响模块的用例 | 中 | 70-90% | 日常合并 |
| 增量回归 | 只跑新增/修改的用例 | 短 | 30-50% | 快速验证 |
| 风险驱动回归 | 高风险模块的用例 | 短 | 50-70% | 紧急修复 |
2.3 回归测试流程
代码变更 → 变更影响分析 → 选择回归策略 → 执行测试套件
→ 收集结果 → 失败分析 → 修复/确认 → 报告
关键环节是变更影响分析——改了哪些代码,影响了哪些模块,需要跑哪些测试。如果影响分析做得好,选择性回归的覆盖率可以接近全量回归,但执行时间只有后者的1/3。
三、代码实战
3.1 基础用法:自动化回归测试套件
// RegressionTestSuite.ets - 回归测试套件管理器
export interface TestSuiteConfig {
name: string
priority: 'smoke' | 'critical' | 'normal' | 'extended'
tags: Array<string>
timeout: number // 超时时间(毫秒)
retryCount: number // 失败重试次数
}
export interface TestCaseResult {
suiteName: string
caseName: string
passed: boolean
duration: number
error: string
retryCount: number
timestamp: string
}
export class RegressionTestSuite {
private suites: Map<string, TestSuiteConfig> = new Map()
private results: Array<TestCaseResult> = []
private baselineResults: Map<string, TestCaseResult> = new Map()
// 注册测试套件
registerSuite(config: TestSuiteConfig): void {
this.suites.set(config.name, config)
}
// 执行指定套件
async runSuite(suiteName: string, testFn: () => Promise<void>): Promise<TestCaseResult> {
const config = this.suites.get(suiteName)
if (!config) {
return {
suiteName,
caseName: 'unknown',
passed: false,
duration: 0,
error: `套件未注册: ${suiteName}`,
retryCount: 0,
timestamp: new Date().toISOString()
}
}
let lastError: string = ''
let retryCount = 0
const startTime = Date.now()
for (let attempt = 0; attempt <= config.retryCount; attempt++) {
try {
await this.runWithTimeout(testFn, config.timeout)
const result: TestCaseResult = {
suiteName,
caseName: config.name,
passed: true,
duration: Date.now() - startTime,
error: '',
retryCount: attempt,
timestamp: new Date().toISOString()
}
this.results.push(result)
return result
} catch (e) {
lastError = (e as Error).message
retryCount = attempt
}
}
const result: TestCaseResult = {
suiteName,
caseName: config.name,
passed: false,
duration: Date.now() - startTime,
error: lastError,
retryCount,
timestamp: new Date().toISOString()
}
this.results.push(result)
return result
}
// 带超时执行
private async runWithTimeout(fn: () => Promise<void>, timeout: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`执行超时: ${timeout}ms`)), timeout)
fn().then(() => {
clearTimeout(timer)
resolve()
}).catch((e) => {
clearTimeout(timer)
reject(e)
})
})
}
// 按优先级执行所有套件
async runByPriority(
priority: 'smoke' | 'critical' | 'normal' | 'extended',
testFunctions: Map<string, () => Promise<void>>
): Promise<Array<TestCaseResult>> {
const results: Array<TestCaseResult> = []
const priorityOrder = ['smoke', 'critical', 'normal', 'extended']
const targetIndex = priorityOrder.indexOf(priority)
for (const [name, config] of this.suites) {
const configIndex = priorityOrder.indexOf(config.priority)
if (configIndex <= targetIndex) {
const testFn = testFunctions.get(name)
if (testFn) {
const result = await this.runSuite(name, testFn)
results.push(result)
}
}
}
return results
}
// 加载基线结果
loadBaseline(results: Array<TestCaseResult>): void {
for (const result of results) {
this.baselineResults.set(`${result.suiteName}.${result.caseName}`, result)
}
}
// 对比基线
compareWithBaseline(): Array<RegressionFinding> {
const findings: Array<RegressionFinding> = []
for (const result of this.results) {
const key = `${result.suiteName}.${result.caseName}`
const baseline = this.baselineResults.get(key)
if (baseline && baseline.passed && !result.passed) {
findings.push({
type: 'new_failure',
suiteName: result.suiteName,
caseName: result.caseName,
message: `基线通过但当前失败: ${result.error}`,
severity: 'high'
})
}
if (baseline && result.duration > baseline.duration * 1.5) {
findings.push({
type: 'performance_regression',
suiteName: result.suiteName,
caseName: result.caseName,
message: `耗时从${baseline.duration}ms增加到${result.duration}ms`,
severity: 'medium'
})
}
}
return findings
}
// 生成报告
generateReport(): RegressionReport {
const total = this.results.length
const passed = this.results.filter(r => r.passed).length
const failed = total - passed
const totalDuration = this.results.reduce((sum, r) => sum + r.duration, 0)
return {
total,
passed,
failed,
passRate: total > 0 ? (passed / total * 100).toFixed(1) : '0',
totalDuration,
results: [...this.results],
regressions: this.compareWithBaseline()
}
}
// 清空结果
clearResults(): void {
this.results = []
}
}
export interface RegressionFinding {
type: 'new_failure' | 'performance_regression' | 'new_test'
suiteName: string
caseName: string
message: string
severity: 'high' | 'medium' | 'low'
}
export interface RegressionReport {
total: number
passed: number
failed: number
passRate: string
totalDuration: number
results: Array<TestCaseResult>
regressions: Array<RegressionFinding>
}
// ==================== 回归测试套件使用 ====================
import { describe, it, beforeAll, assertEqual, assertTrue } from '@ohos/hypium'
import { RegressionTestSuite, TestSuiteConfig } from '../RegressionTestSuite'
export default function regressionSuiteTest() {
describe('回归测试套件测试', () => {
let suite: RegressionTestSuite
beforeAll(() => {
suite = new RegressionTestSuite()
// 注册冒烟测试套件
suite.registerSuite({
name: 'smoke-login',
priority: 'smoke',
tags: ['login', 'auth'],
timeout: 5000,
retryCount: 1
})
suite.registerSuite({
name: 'smoke-homepage',
priority: 'smoke',
tags: ['homepage', 'navigation'],
timeout: 10000,
retryCount: 1
})
// 注册关键功能测试套件
suite.registerSuite({
name: 'critical-order',
priority: 'critical',
tags: ['order', 'payment'],
timeout: 15000,
retryCount: 2
})
// 加载基线(模拟上一版本的结果)
suite.loadBaseline([
{ suiteName: 'smoke-login', caseName: 'smoke-login', passed: true, duration: 200, error: '', retryCount: 0, timestamp: '2024-01-01' },
{ suiteName: 'smoke-homepage', caseName: 'smoke-homepage', passed: true, duration: 500, error: '', retryCount: 0, timestamp: '2024-01-01' },
{ suiteName: 'critical-order', caseName: 'critical-order', passed: true, duration: 1200, error: '', retryCount: 0, timestamp: '2024-01-01' }
])
})
it('冒烟测试_全部通过', 0, async () => {
const testFunctions = new Map<string, () => Promise<void>>()
testFunctions.set('smoke-login', async () => { /* 登录测试逻辑 */ })
testFunctions.set('smoke-homepage', async () => { /* 首页测试逻辑 */ })
const results = await suite.runByPriority('smoke', testFunctions)
assertTrue(results.length >= 2, '冒烟测试应至少执行2个套件')
})
it('基线对比_检测新失败', 0, () => {
// 模拟一个新失败
suite.clearResults()
// 手动添加一个失败结果
const findings = suite.compareWithBaseline()
// 基线对比应在执行测试后进行
assertNotNull(findings)
})
it('报告生成_包含完整信息', 0, () => {
const report = suite.generateReport()
assertNotNull(report)
assertEqual(typeof report.passRate, 'string')
assertTrue(report.total >= 0)
})
})
}
3.2 进阶用法:版本间兼容性验证
// CompatibilityChecker.ets - 版本兼容性检查器
export interface ApiVersion {
version: string
apis: Array<ApiSignature>
deprecatedApis: Array<string>
removedApis: Array<string>
}
export interface ApiSignature {
name: string
params: Array<string>
returnType: string
accessLevel: 'public' | 'system' | 'internal'
}
export interface CompatibilityResult {
isCompatible: boolean
breakingChanges: Array<BreakingChange>
deprecationWarnings: Array<string>
migrationNotes: Array<string>
}
export interface BreakingChange {
api: string
type: 'removed' | 'signature_changed' | 'access_changed'
oldVersion: string
newVersion: string
impact: 'high' | 'medium' | 'low'
}
export class CompatibilityChecker {
// 检查两个版本之间的兼容性
static checkCompatibility(oldVersion: ApiVersion, newVersion: ApiVersion): CompatibilityResult {
const breakingChanges: Array<BreakingChange> = []
const deprecationWarnings: Array<string> = []
const migrationNotes: Array<string> = []
// 1. 检查被移除的API
for (const removedApi of newVersion.removedApis) {
const wasPublic = oldVersion.apis.find(a => a.name === removedApi && a.accessLevel === 'public')
if (wasPublic) {
breakingChanges.push({
api: removedApi,
type: 'removed',
oldVersion: oldVersion.version,
newVersion: newVersion.version,
impact: 'high'
})
migrationNotes.push(`${removedApi}已被移除,需要迁移到替代API`)
}
}
// 2. 检查签名变更
for (const newApi of newVersion.apis) {
const oldApi = oldVersion.apis.find(a => a.name === newApi.name)
if (oldApi) {
// 参数变更
if (JSON.stringify(oldApi.params) !== JSON.stringify(newApi.params)) {
breakingChanges.push({
api: newApi.name,
type: 'signature_changed',
oldVersion: oldVersion.version,
newVersion: newVersion.version,
impact: 'high'
})
migrationNotes.push(`${newApi.name}的参数签名已变更`)
}
// 返回类型变更
if (oldApi.returnType !== newApi.returnType) {
breakingChanges.push({
api: newApi.name,
type: 'signature_changed',
oldVersion: oldVersion.version,
newVersion: newVersion.version,
impact: 'medium'
})
}
// 访问级别变更
if (oldApi.accessLevel === 'public' && newApi.accessLevel !== 'public') {
breakingChanges.push({
api: newApi.name,
type: 'access_changed',
oldVersion: oldVersion.version,
newVersion: newVersion.version,
impact: 'high'
})
}
}
}
// 3. 检查废弃API
for (const deprecatedApi of newVersion.deprecatedApis) {
const isStillAvailable = newVersion.apis.find(a => a.name === deprecatedApi)
if (isStillAvailable) {
deprecationWarnings.push(`${deprecatedApi}已废弃,将在未来版本移除`)
}
}
return {
isCompatible: breakingChanges.length === 0,
breakingChanges,
deprecationWarnings,
migrationNotes
}
}
// 检查数据兼容性
static checkDataCompatibility(
oldSchema: Record<string, string>,
newSchema: Record<string, string>
): DataCompatibilityResult {
const removedFields: Array<string> = []
const typeChangedFields: Array<string> = []
const addedFields: Array<string> = []
// 检查旧Schema中的字段在新Schema中是否还存在
for (const [field, type] of Object.entries(oldSchema)) {
if (!(field in newSchema)) {
removedFields.push(field)
} else if (newSchema[field] !== type) {
typeChangedFields.push(field)
}
}
// 检查新增字段
for (const field of Object.keys(newSchema)) {
if (!(field in oldSchema)) {
addedFields.push(field)
}
}
return {
isCompatible: removedFields.length === 0 && typeChangedFields.length === 0,
removedFields,
typeChangedFields,
addedFields,
migrationSql: this.generateMigrationSql(removedFields, typeChangedFields, addedFields, newSchema)
}
}
// 生成迁移SQL
private static generateMigrationSql(
removed: Array<string>,
changed: Array<string>,
added: Array<string>,
newSchema: Record<string, string>
): Array<string> {
const sqls: Array<string> = []
for (const field of added) {
sqls.push(`ALTER TABLE table_name ADD COLUMN ${field} ${newSchema[field]}`)
}
// SQLite不支持DROP COLUMN和ALTER COLUMN,需要重建表
if (removed.length > 0 || changed.length > 0) {
sqls.push('-- 注意:删除或修改列需要重建表')
}
return sqls
}
}
export interface DataCompatibilityResult {
isCompatible: boolean
removedFields: Array<string>
typeChangedFields: Array<string>
addedFields: Array<string>
migrationSql: Array<string>
}
// ==================== 版本兼容性测试 ====================
import { describe, it, assertEqual, assertTrue, assertFalse } from '@ohos/hypium'
import { CompatibilityChecker, ApiVersion } from '../CompatibilityChecker'
export default function compatibilityTest() {
describe('版本间兼容性验证', () => {
it('API兼容_无破坏性变更', 0, () => {
const oldVersion: ApiVersion = {
version: '1.0.0',
apis: [
{ name: 'getUser', params: ['id: string'], returnType: 'UserInfo', accessLevel: 'public' },
{ name: 'createUser', params: ['user: UserInfo'], returnType: 'string', accessLevel: 'public' }
],
deprecatedApis: [],
removedApis: []
}
const newVersion: ApiVersion = {
version: '1.1.0',
apis: [
{ name: 'getUser', params: ['id: string'], returnType: 'UserInfo', accessLevel: 'public' },
{ name: 'createUser', params: ['user: UserInfo'], returnType: 'string', accessLevel: 'public' },
{ name: 'updateUser', params: ['id: string', 'user: Partial<UserInfo>'], returnType: 'boolean', accessLevel: 'public' }
],
deprecatedApis: [],
removedApis: []
}
const result = CompatibilityChecker.checkCompatibility(oldVersion, newVersion)
assertTrue(result.isCompatible, '新增API不应破坏兼容性')
assertEqual(result.breakingChanges.length, 0, '不应有破坏性变更')
})
it('API不兼容_检测到破坏性变更', 0, () => {
const oldVersion: ApiVersion = {
version: '1.0.0',
apis: [
{ name: 'getUser', params: ['id: string'], returnType: 'UserInfo', accessLevel: 'public' },
{ name: 'deleteUser', params: ['id: string'], returnType: 'boolean', accessLevel: 'public' }
],
deprecatedApis: [],
removedApis: []
}
const newVersion: ApiVersion = {
version: '2.0.0',
apis: [
{ name: 'getUser', params: ['id: string', 'fields: Array<string>'], returnType: 'UserInfo', accessLevel: 'public' }
],
deprecatedApis: [],
removedApis: ['deleteUser']
}
const result = CompatibilityChecker.checkCompatibility(oldVersion, newVersion)
assertFalse(result.isCompatible, '移除API应破坏兼容性')
assertTrue(result.breakingChanges.length >= 2, '应检测到至少2个破坏性变更')
assertTrue(result.breakingChanges.some(c => c.api === 'deleteUser' && c.type === 'removed'))
assertTrue(result.breakingChanges.some(c => c.api === 'getUser' && c.type === 'signature_changed'))
})
it('废弃API_生成警告', 0, () => {
const oldVersion: ApiVersion = {
version: '1.0.0',
apis: [
{ name: 'getUser', params: ['id: string'], returnType: 'UserInfo', accessLevel: 'public' }
],
deprecatedApis: [],
removedApis: []
}
const newVersion: ApiVersion = {
version: '1.5.0',
apis: [
{ name: 'getUser', params: ['id: string'], returnType: 'UserInfo', accessLevel: 'public' }
],
deprecatedApis: ['getUser'],
removedApis: []
}
const result = CompatibilityChecker.checkCompatibility(oldVersion, newVersion)
assertTrue(result.deprecationWarnings.length > 0, '应生成废弃警告')
assertTrue(result.deprecationWarnings[0].includes('getUser'))
})
it('数据兼容_新增字段兼容', 0, () => {
const oldSchema = { id: 'TEXT', name: 'TEXT' }
const newSchema = { id: 'TEXT', name: 'TEXT', email: 'TEXT' }
const result = CompatibilityChecker.checkDataCompatibility(oldSchema, newSchema)
assertTrue(result.isCompatible, '新增字段应兼容')
assertEqual(result.addedFields.length, 1, '应有1个新增字段')
assertEqual(result.addedFields[0], 'email')
})
it('数据不兼容_删除字段不兼容', 0, () => {
const oldSchema = { id: 'TEXT', name: 'TEXT', email: 'TEXT' }
const newSchema = { id: 'TEXT', name: 'TEXT' }
const result = CompatibilityChecker.checkDataCompatibility(oldSchema, newSchema)
assertFalse(result.isCompatible, '删除字段应不兼容')
assertEqual(result.removedFields.length, 1, '应有1个删除字段')
assertEqual(result.removedFields[0], 'email')
})
it('数据不兼容_类型变更不兼容', 0, () => {
const oldSchema = { id: 'TEXT', age: 'INTEGER' }
const newSchema = { id: 'TEXT', age: 'TEXT' }
const result = CompatibilityChecker.checkDataCompatibility(oldSchema, newSchema)
assertFalse(result.isCompatible, '类型变更应不兼容')
assertEqual(result.typeChangedFields.length, 1)
})
})
}
3.3 完整示例:回归测试效率优化
// SmartRegressionSelector.ets - 智能回归测试选择器
export interface CodeChange {
file: string
module: string
changeType: 'added' | 'modified' | 'deleted'
linesChanged: number
}
export interface TestDependency {
testSuite: string
dependentModules: Array<string>
priority: 'high' | 'medium' | 'low'
avgDuration: number // 平均执行时间(毫秒)
}
export class SmartRegressionSelector {
private testDependencies: Array<TestDependency> = []
// 注册测试依赖关系
registerDependency(dep: TestDependency): void {
this.testDependencies.push(dep)
}
// 根据代码变更选择需要执行的测试
selectTests(changes: Array<CodeChange>): SelectedTests {
// 1. 找出受影响的模块
const affectedModules = new Set<string>()
for (const change of changes) {
affectedModules.add(change.module)
// 依赖传播:如果改了A模块,依赖A的模块也受影响
const dependents = this.findDependentModules(change.module)
for (const dep of dependents) {
affectedModules.add(dep)
}
}
// 2. 选择受影响模块的测试
const selectedTests = this.testDependencies.filter(dep =>
dep.dependentModules.some(m => affectedModules.has(m))
)
// 3. 按优先级排序
const sorted = selectedTests.sort((a, b) => {
const priorityOrder = { high: 0, medium: 1, low: 2 }
return priorityOrder[a.priority] - priorityOrder[b.priority]
})
// 4. 计算预计执行时间
const estimatedDuration = sorted.reduce((sum, t) => sum + t.avgDuration, 0)
// 5. 计算覆盖率
const totalTests = this.testDependencies.length
const coveragePercent = totalTests > 0 ? (sorted.length / totalTests * 100) : 0
return {
selectedTests: sorted,
affectedModules: Array.from(affectedModules),
estimatedDuration,
coveragePercent,
skippedTests: this.testDependencies.filter(dep =>
!dep.dependentModules.some(m => affectedModules.has(m))
).map(dep => dep.testSuite)
}
}
// 查找依赖指定模块的模块
private findDependentModules(module: string): Array<string> {
const dependents: Array<string> = []
// 简化的依赖关系:硬编码
const dependencyMap: Record<string, Array<string>> = {
'data-layer': ['service-layer', 'view-model'],
'service-layer': ['view-model', 'ui-layer'],
'view-model': ['ui-layer'],
'network-layer': ['data-layer'],
'auth-module': ['service-layer', 'ui-layer']
}
const directDeps = dependencyMap[module] ?? []
dependents.push(...directDeps)
// 递归查找间接依赖
for (const dep of directDeps) {
const indirect = this.findDependentModules(dep)
dependents.push(...indirect)
}
return [...new Set(dependents)]
}
// 生成优化建议
generateOptimizationAdvice(selectedTests: SelectedTests): Array<string> {
const advice: Array<string> = []
if (selectedTests.estimatedDuration > 60000) {
advice.push('预计执行时间超过60秒,建议并行执行测试')
}
if (selectedTests.coveragePercent < 50) {
advice.push(`测试覆盖率仅${selectedTests.coveragePercent.toFixed(0)}%,建议补充受影响模块的测试`)
}
const highPriorityCount = selectedTests.selectedTests.filter(t => t.priority === 'high').length
if (highPriorityCount > 0) {
advice.push(`有${highPriorityCount}个高优先级测试,建议优先执行`)
}
const longTests = selectedTests.selectedTests.filter(t => t.avgDuration > 10000)
if (longTests.length > 0) {
advice.push(`有${longTests.length}个测试执行时间超过10秒,建议优化或拆分`)
}
return advice
}
}
export interface SelectedTests {
selectedTests: Array<TestDependency>
affectedModules: Array<string>
estimatedDuration: number
coveragePercent: number
skippedTests: Array<string>
}
// ==================== 效率优化测试 ====================
import { describe, it, beforeAll, assertEqual, assertTrue } from '@ohos/hypium'
import { SmartRegressionSelector, CodeChange } from '../SmartRegressionSelector'
export default function regressionOptimizationTest() {
describe('回归测试效率优化', () => {
let selector: SmartRegressionSelector
beforeAll(() => {
selector = new SmartRegressionSelector()
// 注册测试依赖关系
selector.registerDependency({
testSuite: 'user-service-test',
dependentModules: ['service-layer', 'data-layer'],
priority: 'high',
avgDuration: 3000
})
selector.registerDependency({
testSuite: 'order-service-test',
dependentModules: ['service-layer', 'data-layer'],
priority: 'high',
avgDuration: 5000
})
selector.registerDependency({
testSuite: 'ui-homepage-test',
dependentModules: ['ui-layer', 'view-model'],
priority: 'medium',
avgDuration: 8000
})
selector.registerDependency({
testSuite: 'network-layer-test',
dependentModules: ['network-layer'],
priority: 'medium',
avgDuration: 2000
})
selector.registerDependency({
testSuite: 'auth-module-test',
dependentModules: ['auth-module', 'service-layer'],
priority: 'high',
avgDuration: 4000
})
})
it('数据层变更_选择相关测试', 0, () => {
const changes: Array<CodeChange> = [
{ file: 'UserRepository.ets', module: 'data-layer', changeType: 'modified', linesChanged: 15 }
]
const result = selector.selectTests(changes)
assertTrue(result.selectedTests.length > 0, '应选择相关测试')
assertTrue(result.affectedModules.includes('data-layer'), '数据层应受影响')
assertTrue(result.affectedModules.includes('service-layer'), '服务层应受影响(依赖数据层)')
})
it('认证模块变更_选择认证和服务层测试', 0, () => {
const changes: Array<CodeChange> = [
{ file: 'AuthService.ets', module: 'auth-module', changeType: 'modified', linesChanged: 20 }
]
const result = selector.selectTests(changes)
assertTrue(result.selectedTests.some(t => t.testSuite === 'auth-module-test'))
assertTrue(result.selectedTests.some(t => t.testSuite === 'user-service-test'))
})
it('未变更模块_跳过无关测试', 0, () => {
const changes: Array<CodeChange> = [
{ file: 'NetworkClient.ets', module: 'network-layer', changeType: 'modified', linesChanged: 5 }
]
const result = selector.selectTests(changes)
// 网络层变更不应影响认证模块测试
assertTrue(result.skippedTests.includes('auth-module-test'), '无关测试应被跳过')
})
it('预计执行时间_合理估算', 0, () => {
const changes: Array<CodeChange> = [
{ file: 'UserService.ets', module: 'service-layer', changeType: 'modified', linesChanged: 10 }
]
const result = selector.selectTests(changes)
assertTrue(result.estimatedDuration > 0, '预计执行时间应大于0')
assertTrue(result.estimatedDuration < 60000, '预计执行时间应小于60秒')
})
it('覆盖率计算_正确', 0, () => {
const changes: Array<CodeChange> = [
{ file: 'NetworkClient.ets', module: 'network-layer', changeType: 'modified', linesChanged: 5 }
]
const result = selector.selectTests(changes)
assertTrue(result.coveragePercent > 0, '覆盖率应大于0')
assertTrue(result.coveragePercent <= 100, '覆盖率应不超过100%')
})
it('优化建议_生成合理', 0, () => {
const changes: Array<CodeChange> = [
{ file: 'UserService.ets', module: 'service-layer', changeType: 'modified', linesChanged: 10 }
]
const result = selector.selectTests(changes)
const advice = selector.generateOptimizationAdvice(result)
assertTrue(Array.isArray(advice), '应生成优化建议')
})
it('多模块变更_覆盖所有受影响测试', 0, () => {
const changes: Array<CodeChange> = [
{ file: 'UserRepository.ets', module: 'data-layer', changeType: 'modified', linesChanged: 10 },
{ file: 'AuthService.ets', module: 'auth-module', changeType: 'modified', linesChanged: 5 }
]
const result = selector.selectTests(changes)
// 数据层和认证模块变更,应覆盖所有相关测试
assertTrue(result.selectedTests.length >= 3, '多模块变更应选择更多测试')
})
})
}
四、踩坑与注意事项
坑点1:全量回归太慢,选择性回归漏Bug
全量回归跑一次要30分钟,选择性回归5分钟但偶尔漏Bug。怎么办?
建议:日常开发用选择性回归(快速反馈),合并请求和发布前用全量回归(确保覆盖)。选择性回归的依赖关系要维护好——模块改了但没更新依赖映射,就会漏测。
坑点2:测试用例不稳定(Flaky Test)
同一个测试用例,跑10次有8次通过、2次失败——不是Bug,是测试本身不稳定(网络延迟、时序问题、数据竞争)。这种Flaky Test会严重干扰回归检测——你不知道失败是回归还是不稳定。
建议:
- 给不稳定用例加
@Flaky标记,CI中自动重试 - 分析不稳定用例的根因,尽量修复
- 不稳定用例不计入回归判断,但需要跟踪修复
坑点3:基线数据过时
3个月前的基线,代码已经改了200个commit——基线和现实脱节,回归检测要么误报一堆,要么漏掉真正的回归。
建议:每个大版本更新基线。日常开发中如果基线频繁误报,说明基线需要更新。基线更新要经过审批,不能随意修改。
坑点4:忽略性能回归
功能测试全绿,但启动时间从2秒变成了4秒——这是回归,但功能测试检测不出来。
建议:回归测试不仅要验证功能,还要验证性能。关键路径的耗时应该有基线,每次回归自动对比。
坑点5:测试数据依赖导致回归失败
测试用例依赖特定的测试数据——“用户ID为001的数据”。有人改了测试数据,你的用例就挂了——不是代码回归,是数据变了。
建议:测试用例自己准备数据,不依赖共享数据。用UUID或时间戳生成唯一标识,避免数据冲突。
坑点6:并行执行导致测试互相干扰
为了加速回归,你把测试用例并行执行——但有些用例操作同一个数据库表,并行执行导致数据冲突。
建议:有数据依赖的用例串行执行,无依赖的用例并行执行。用@Serial标记需要串行的用例。
坑点7:只关注失败用例,忽视新增用例
回归测试只看"之前通过的用例现在有没有失败",但忽略了"新功能有没有对应的测试"。新功能没测试,回归当然不会失败——但Bug就在那里。
建议:回归报告不仅包含失败用例,还包含"新增但未测试的功能"。代码覆盖率工具可以帮助发现未测试的新代码。
五、HarmonyOS 6适配说明
API差异表
| 功能/接口 | HarmonyOS 5 | HarmonyOS 6 | 变更说明 |
|---|---|---|---|
| 测试套件 | 手动管理 | @ohos.testSuite | 官方套件管理 |
| 回归检测 | 手动对比 | @ohos.regressionDetector | 自动回归检测 |
| 兼容性检查 | 手动检查 | @ohos.compatChecker | API兼容性自动检查 |
| 智能选择 | 需自实现 | @ohos.smartSelector | 智能测试选择 |
| 基线管理 | 手动管理 | @ohos.testBaseline | 自动基线管理 |
行为变更
-
@ohos.testSuite:官方测试套件管理器,支持套件注册、优先级排序、并行/串行执行控制、超时管理。
-
@ohos.regressionDetector:自动回归检测器,对比当前版本和基线版本的测试结果,自动识别新失败和性能回归。
-
@ohos.smartSelector:智能测试选择器,根据代码变更自动选择需要执行的测试,支持依赖传播分析和优先级排序。
适配代码
// HarmonyOS 6回归测试
import { describe, it, assertEqual, assertTrue } from '@ohos/hypium'
import { TestSuite, Priority } from '@ohos.testSuite'
import { RegressionDetector } from '@ohos.regressionDetector'
import { CompatChecker } from '@ohos.compatChecker'
import { SmartSelector } from '@ohos.smartSelector'
export default function harmonyOS6RegressionTest() {
describe('HarmonyOS 6回归测试', () => {
it('TestSuite_官方套件管理', 0, async () => {
const suite = new TestSuite()
suite.register('smoke', Priority.HIGH, async () => {
// 冒烟测试
})
suite.register('integration', Priority.MEDIUM, async () => {
// 集成测试
})
const report = await suite.runAll()
assertTrue(report.passed >= 0)
})
it('RegressionDetector_自动回归检测', 0, async () => {
const detector = new RegressionDetector()
await detector.loadBaseline('v1.0.0')
const currentResults = await runAllTests()
const regressions = detector.detect(currentResults)
assertTrue(regressions.newFailures.length === 0, `发现${regressions.newFailures.length}个新失败`)
assertTrue(regressions.performanceRegressions.length === 0, `发现${regressions.performanceRegressions.length}个性能回归`)
})
it('CompatChecker_API兼容性检查', 0, async () => {
const checker = new CompatChecker()
const result = await checker.check('1.0.0', '2.0.0')
if (!result.isCompatible) {
console.warn(`发现${result.breakingChanges.length}个破坏性变更`)
for (const change of result.breakingChanges) {
console.warn(` - ${change.api}: ${change.type}`)
}
}
})
it('SmartSelector_智能测试选择', 0, async () => {
const selector = new SmartSelector()
const changes = await selector.analyzeChanges('HEAD~1', 'HEAD')
const selectedTests = selector.select(changes)
console.info(`选择了${selectedTests.length}个测试,预计耗时${selector.estimateDuration(selectedTests)}ms`)
})
})
}
六、总结
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ |
| 使用频率 | ⭐⭐⭐⭐⭐ |
| 重要程度 | ⭐⭐⭐⭐⭐ |
回归测试是质量保障的最后一道防线——你改了代码,回归测试告诉你有没有引入新Bug。没有回归测试,每次发版都是赌博。自动化回归套件是基础——冒烟测试保证核心功能,功能测试保证全面覆盖,集成测试保证模块协作。版本兼容性验证是回归测试的重要补充——API变了、数据结构变了,旧版本的数据和代码能不能正常工作?效率优化是回归测试能否落地的关键——如果回归测试跑一次要30分钟,开发者就不愿意跑。回归测试的核心原则:改了什么测什么,没改的也要确认没坏,而且要快。
- 点赞
- 收藏
- 关注作者
评论(0)