HarmonyOS开发:回归测试——版本回归测试

举报
Jack20 发表于 2026/06/24 16:32:31 2026/06/24
【摘要】 HarmonyOS开发:回归测试——版本回归测试📌 核心要点:改了A模块,B模块挂了——这种"改一个坏一个"的回归Bug是开发者的噩梦。回归测试就是每次代码变更后自动验证"改了的东西该改,没改的东西没坏",用自动化套件守护每一个版本的质量底线。 一、背景与动机你有没有经历过这种场景?修了一个Bug,结果引入了两个新Bug。优化了启动速度,结果页面加载变慢了。升级了第三方库,结果某个功能直...

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 自动基线管理

行为变更

  1. @ohos.testSuite:官方测试套件管理器,支持套件注册、优先级排序、并行/串行执行控制、超时管理。

  2. @ohos.regressionDetector:自动回归检测器,对比当前版本和基线版本的测试结果,自动识别新失败和性能回归。

  3. @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分钟,开发者就不愿意跑。回归测试的核心原则:改了什么测什么,没改的也要确认没坏,而且要快

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。