HarmonyOS开发:测试报告与测试结果报告

举报
Jack20 发表于 2026/06/24 15:18:15 2026/06/24
【摘要】 HarmonyOS开发:测试报告与测试结果报告📌 核心要点:测试跑完了,然后呢?一堆绿色红色你看得懂吗?测试报告是测试的"成绩单"——它不仅要告诉你"过了多少、挂了多少",还要告诉你"挂在哪里、为什么挂、怎么修"。学会生成、解读、定制测试报告,才能让测试结果真正指导开发决策。 一、背景与动机你跑完100个测试用例,控制台刷了一屏日志,3个红的97个绿的。然后呢?你要花多长时间找到那3个失...

HarmonyOS开发:测试报告与测试结果报告

📌 核心要点:测试跑完了,然后呢?一堆绿色红色你看得懂吗?测试报告是测试的"成绩单"——它不仅要告诉你"过了多少、挂了多少",还要告诉你"挂在哪里、为什么挂、怎么修"。学会生成、解读、定制测试报告,才能让测试结果真正指导开发决策。


一、背景与动机

你跑完100个测试用例,控制台刷了一屏日志,3个红的97个绿的。然后呢?你要花多长时间找到那3个失败的用例?失败的原因是什么?是断言错了还是环境有问题?上次跑是几个红的?趋势是变好还是变差?

如果这些问题你答不上来,那你的测试报告就是"一堆日志",不是"可操作的信息"。

好的测试报告应该做到三件事:

  1. 一目了然:扫一眼就知道整体状况——通过率、失败数、耗时
  2. 快速定位:点进去就能看到失败原因、堆栈、相关代码
  3. 趋势追踪:和历史数据对比,知道质量是在变好还是变差

Hypium默认的测试输出是控制台文本,信息密度低、不可存档、不支持对比。在CI环境和团队协作中,你需要更专业的报告方案。


二、核心原理

2.1 测试报告生成流程

flowchart TB
    A[测试执行] --> B[结果采集]
    B --> C[数据结构化]
    C --> D{报告格式}
    D --> E[HTML可视化报告]
    D --> F[JSON结构化数据]
    D --> G[XML/JUnit格式]
    D --> H[自定义格式]

    E --> E1[浏览器查看]
    E --> E2[团队共享]
    F --> F1[CI集成]
    F --> F2[数据分析]
    G --> G1[Jenkins兼容]
    G --> G2[第三方工具]
    H --> H1[企业定制]
    H --> H2[自动化处理]

    classDef execStyle fill:#4CAF50,stroke:#388E3C,color:#fff,font-weight:bold
    classDef collectStyle fill:#2196F3,stroke:#1976D2,color:#fff
    classDef formatStyle fill:#FF9800,stroke:#F57C00,color:#fff
    classDef htmlStyle fill:#9C27B0,stroke:#7B1FA2,color:#fff
    classDef jsonStyle fill:#F44336,stroke:#D32F2F,color:#fff
    classDef xmlStyle fill:#607D8B,stroke:#455A64,color:#fff
    classDef customStyle fill:#795548,stroke:#5D4037,color:#fff

    class A,B,C execStyle
    class D collectStyle
    class E,E1,E2 htmlStyle
    class F,F1,F2 jsonStyle
    class G,G1,G2 xmlStyle
    class H,H1,H2 customStyle

2.2 测试报告核心数据结构

一份完整的测试报告包含以下信息:

数据层级 包含内容 用途
套件级 套件名称、总用例数、通过/失败/跳过数、总耗时 全局概览
用例级 用例名称、状态、耗时、断言数 详细结果
失败详情 错误消息、堆栈跟踪、预期值/实际值 问题定位
环境信息 设备型号、系统版本、测试时间 环境追溯
覆盖率 行/分支/函数覆盖率 质量度量

2.3 报告格式对比

格式 优点 缺点 适用场景
控制台文本 零配置,即时查看 不可存档,信息有限 本地开发调试
HTML 可视化,易读,可共享 需要浏览器打开 团队共享、归档
JSON 结构化,易解析 不直观 CI集成、自动化
XML(JUnit) 兼容性好 格式冗长 Jenkins等CI工具
Markdown 轻量,可读 功能有限 PR评论、文档

三、代码实战

3.1 基础用法:Hypium默认报告与命令行输出

// ReportDemo.ets - 用于演示报告的测试代码
import { describe, it, beforeAll, afterAll } from '@ohos/hypium'

export default function reportDemoTest() {
  describe('报告演示测试套件', () => {

    beforeAll(() => {
      console.info('测试套件开始执行')
    })

    afterAll(() => {
      console.info('测试套件执行完毕')
    })

    it('add_正常相加_结果正确', 0, () => {
      assertEqual(1 + 1, 2)
    })

    it('subtract_正常相减_结果正确', 0, () => {
      assertEqual(10 - 3, 7)
    })

    it('multiply_正常相乘_结果正确', 0, () => {
      assertEqual(4 * 5, 20)
    })

    it('divide_除以零_抛出异常', 0, () => {
      assertThrowError(() => {
        const result = 10 / 0  // 这不会抛异常,JS中除以零返回Infinity
        throw new Error('除数不能为零')  // 手动抛出
      })
    })

    it('string_字符串拼接_结果正确', 0, () => {
      assertEqual('hello' + ' ' + 'world', 'hello world')
    })
  })
}

运行测试并获取报告的命令:

# 方式1:DevEco Studio中运行,结果在Test面板中显示

# 方式2:命令行运行,控制台输出
hdc shell aa test -b com.example.myapp -m entry_test

# 方式3:指定测试类运行
hdc shell aa test -b com.example.myapp -m entry_test -s class ReportDemoTest

# 方式4:输出到文件
hdc shell aa test -b com.example.myapp -m entry_test > test_result.txt 2>&1

3.2 进阶用法:自定义测试报告生成器

// TestReportGenerator.ets - 自定义测试报告生成器
export interface TestCaseResult {
  name: string
  suite: string
  status: 'passed' | 'failed' | 'skipped' | 'error'
  duration: number  // 毫秒
  errorMessage?: string
  stackTrace?: string
  expectedValue?: string
  actualValue?: string
  timestamp: number
}

export interface TestSuiteResult {
  name: string
  totalCases: number
  passedCases: number
  failedCases: number
  skippedCases: number
  errorCases: number
  totalDuration: number
  cases: TestCaseResult[]
}

export interface TestReport {
  projectName: string
  buildNumber: string
  timestamp: number
  deviceInfo: string
  totalSuites: number
  totalCases: number
  passedCases: number
  failedCases: number
  skippedCases: number
  errorCases: number
  totalDuration: number
  suites: TestSuiteResult[]
}

export class TestReportGenerator {
  private report: TestReport

  constructor(projectName: string, buildNumber: string = 'local') {
    this.report = {
      projectName,
      buildNumber,
      timestamp: Date.now(),
      deviceInfo: 'HarmonyOS Device',
      totalSuites: 0,
      totalCases: 0,
      passedCases: 0,
      failedCases: 0,
      skippedCases: 0,
      errorCases: 0,
      totalDuration: 0,
      suites: []
    }
  }

  setDeviceInfo(info: string): void {
    this.report.deviceInfo = info
  }

  addSuiteResult(suite: TestSuiteResult): void {
    this.report.suites.push(suite)
    this.report.totalSuites++
    this.report.totalCases += suite.totalCases
    this.report.passedCases += suite.passedCases
    this.report.failedCases += suite.failedCases
    this.report.skippedCases += suite.skippedCases
    this.report.errorCases += suite.errorCases
    this.report.totalDuration += suite.totalDuration
  }

  getPassRate(): number {
    if (this.report.totalCases === 0) return 0
    return Math.round((this.report.passedCases / this.report.totalCases) * 10000) / 100
  }

  // 生成JSON格式报告
  generateJson(): string {
    return JSON.stringify(this.report, null, 2)
  }

  // 生成HTML格式报告
  generateHtml(): string {
    const passRate = this.getPassRate()
    const statusColor = passRate >= 90 ? '#4CAF50' : passRate >= 70 ? '#FF9800' : '#F44336'

    let html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>测试报告 - ${this.report.projectName}</title>
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 20px; background: #f5f5f5; }
    .header { background: ${statusColor}; color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
    .header h1 { margin: 0 0 10px 0; }
    .summary { display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 20px; }
    .stat-card { background: white; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); min-width: 120px; }
    .stat-card .number { font-size: 28px; font-weight: bold; }
    .stat-card .label { color: #666; font-size: 14px; }
    .suite { background: white; border-radius: 8px; margin-bottom: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; }
    .suite-header { padding: 12px 15px; background: #fafafa; border-bottom: 1px solid #eee; cursor: pointer; }
    .suite-header h3 { margin: 0; }
    .case-list { padding: 0; }
    .case { padding: 8px 15px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; }
    .case:last-child { border-bottom: none; }
    .case-name { font-size: 14px; }
    .case-duration { color: #999; font-size: 12px; }
    .status-passed { color: #4CAF50; }
    .status-failed { color: #F44336; }
    .status-skipped { color: #9E9E9E; }
    .error-detail { background: #fff3f3; padding: 10px 15px; font-size: 13px; color: #c62828; font-family: monospace; }
    .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; color: white; margin-left: 8px; }
    .badge-passed { background: #4CAF50; }
    .badge-failed { background: #F44336; }
    .badge-skipped { background: #9E9E9E; }
  </style>
</head>
<body>
  <div class="header">
    <h1>🧪 ${this.report.projectName} 测试报告</h1>
    <p>构建: ${this.report.buildNumber} | 设备: ${this.report.deviceInfo} | 时间: ${new Date(this.report.timestamp).toLocaleString('zh-CN')}</p>
  </div>

  <div class="summary">
    <div class="stat-card">
      <div class="number">${this.report.totalCases}</div>
      <div class="label">总用例</div>
    </div>
    <div class="stat-card">
      <div class="number status-passed">${this.report.passedCases}</div>
      <div class="label">通过</div>
    </div>
    <div class="stat-card">
      <div class="number status-failed">${this.report.failedCases}</div>
      <div class="label">失败</div>
    </div>
    <div class="stat-card">
      <div class="number">${this.report.skippedCases}</div>
      <div class="label">跳过</div>
    </div>
    <div class="stat-card">
      <div class="number">${passRate}%</div>
      <div class="label">通过率</div>
    </div>
    <div class="stat-card">
      <div class="number">${(this.report.totalDuration / 1000).toFixed(1)}s</div>
      <div class="label">总耗时</div>
    </div>
  </div>`

    for (const suite of this.report.suites) {
      const suitePassRate = suite.totalCases > 0
        ? Math.round((suite.passedCases / suite.totalCases) * 100)
        : 0

      html += `
  <div class="suite">
    <div class="suite-header">
      <h3>${suite.name} <span class="badge badge-${suite.failedCases > 0 ? 'failed' : 'passed'}">${suitePassRate}%</span></h3>
    </div>
    <div class="case-list">`

      for (const testCase of suite.cases) {
        html += `
      <div class="case">
        <span class="case-name">
          <span class="status-${testCase.status}">●</span>
          ${testCase.name}
        </span>
        <span class="case-duration">${testCase.duration}ms</span>
      </div>`

        if (testCase.status === 'failed' && testCase.errorMessage) {
          html += `
      <div class="error-detail">
        ${testCase.errorMessage}
        ${testCase.expectedValue ? `<br>预期: ${testCase.expectedValue}` : ''}
        ${testCase.actualValue ? `<br>实际: ${testCase.actualValue}` : ''}
      </div>`
        }
      }

      html += `
    </div>
  </div>`
    }

    html += `
</body>
</html>`

    return html
  }

  // 生成Markdown格式报告
  generateMarkdown(): string {
    const lines: string[] = []
    const passRate = this.getPassRate()

    lines.push(`# 🧪 ${this.report.projectName} 测试报告`)
    lines.push('')
    lines.push(`- **构建号**: ${this.report.buildNumber}`)
    lines.push(`- **设备**: ${this.report.deviceInfo}`)
    lines.push(`- **时间**: ${new Date(this.report.timestamp).toLocaleString('zh-CN')}`)
    lines.push('')
    lines.push('## 概览')
    lines.push('')
    lines.push('| 指标 | 值 |')
    lines.push('|------|-----|')
    lines.push(`| 总用例 | ${this.report.totalCases} |`)
    lines.push(`| ✅ 通过 | ${this.report.passedCases} |`)
    lines.push(`| ❌ 失败 | ${this.report.failedCases} |`)
    lines.push(`| ⏭️ 跳过 | ${this.report.skippedCases} |`)
    lines.push(`| 通过率 | ${passRate}% |`)
    lines.push(`| 总耗时 | ${(this.report.totalDuration / 1000).toFixed(1)}s |`)
    lines.push('')

    if (this.report.failedCases > 0) {
      lines.push('## ❌ 失败用例')
      lines.push('')
      for (const suite of this.report.suites) {
        for (const testCase of suite.cases) {
          if (testCase.status === 'failed') {
            lines.push(`### ${testCase.name}`)
            lines.push(`- **套件**: ${suite.name}`)
            lines.push(`- **耗时**: ${testCase.duration}ms`)
            if (testCase.errorMessage) {
              lines.push(`- **错误**: ${testCase.errorMessage}`)
            }
            if (testCase.expectedValue) {
              lines.push(`- **预期**: \`${testCase.expectedValue}\``)
            }
            if (testCase.actualValue) {
              lines.push(`- **实际**: \`${testCase.actualValue}\``)
            }
            lines.push('')
          }
        }
      }
    }

    lines.push('## 📊 套件详情')
    lines.push('')
    lines.push('| 套件 | 总数 | 通过 | 失败 | 通过率 | 耗时 |')
    lines.push('|------|------|------|------|--------|------|')

    for (const suite of this.report.suites) {
      const rate = suite.totalCases > 0
        ? Math.round((suite.passedCases / suite.totalCases) * 100)
        : 0
      lines.push(`| ${suite.name} | ${suite.totalCases} | ${suite.passedCases} | ${suite.failedCases} | ${rate}% | ${(suite.totalDuration / 1000).toFixed(1)}s |`)
    }

    return lines.join('\n')
  }

  // 生成JUnit XML格式(兼容Jenkins等CI工具)
  generateJUnitXml(): string {
    let xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
    xml += `<testsuites tests="${this.report.totalCases}" failures="${this.report.failedCases}" time="${(this.report.totalDuration / 1000).toFixed(3)}">\n`

    for (const suite of this.report.suites) {
      xml += `  <testsuite name="${suite.name}" tests="${suite.totalCases}" failures="${suite.failedCases}" skipped="${suite.skippedCases}" time="${(suite.totalDuration / 1000).toFixed(3)}">\n`

      for (const testCase of suite.cases) {
        xml += `    <testcase name="${testCase.name}" classname="${suite.name}" time="${(testCase.duration / 1000).toFixed(3)}">\n`

        if (testCase.status === 'failed') {
          xml += `      <failure message="${testCase.errorMessage || 'Assertion failed'}">\n`
          if (testCase.stackTrace) {
            xml += `        ${testCase.stackTrace}\n`
          }
          xml += `      </failure>\n`
        } else if (testCase.status === 'skipped') {
          xml += '      <skipped/>\n'
        }

        xml += '    </testcase>\n'
      }

      xml += '  </testsuite>\n'
    }

    xml += '</testsuites>'
    return xml
  }

  getReport(): TestReport {
    return { ...this.report }
  }
}
// TestReportGeneratorTest.ets - 报告生成器测试
import { describe, it, beforeEach } from '@ohos/hypium'
import { TestReportGenerator, TestSuiteResult } from '../../../main/ets/test/TestReportGenerator'

export default function testReportGeneratorTest() {
  describe('TestReportGenerator测试报告生成器', () => {
    let generator: TestReportGenerator

    beforeEach(() => {
      generator = new TestReportGenerator('MyApp', 'build-123')
    })

    it('addSuiteResult_添加套件结果_统计数据正确', 0, () => {
      const suite: TestSuiteResult = {
        name: 'Calculator测试',
        totalCases: 5,
        passedCases: 4,
        failedCases: 1,
        skippedCases: 0,
        errorCases: 0,
        totalDuration: 500,
        cases: [
          { name: 'add_正常', suite: 'Calculator测试', status: 'passed', duration: 50, timestamp: Date.now() },
          { name: 'subtract_正常', suite: 'Calculator测试', status: 'passed', duration: 80, timestamp: Date.now() },
          { name: 'multiply_正常', suite: 'Calculator测试', status: 'passed', duration: 60, timestamp: Date.now() },
          { name: 'divide_除零', suite: 'Calculator测试', status: 'passed', duration: 100, timestamp: Date.now() },
          { name: 'divide_异常', suite: 'Calculator测试', status: 'failed', duration: 210, timestamp: Date.now(), errorMessage: 'Expected 5 but got NaN' }
        ]
      }

      generator.addSuiteResult(suite)

      assertEqual(generator.getReport().totalCases, 5)
      assertEqual(generator.getReport().passedCases, 4)
      assertEqual(generator.getReport().failedCases, 1)
      assertEqual(generator.getPassRate(), 80)
    })

    it('generateJson_生成JSON报告_格式正确', 0, () => {
      const suite: TestSuiteResult = {
        name: 'StringUtils测试',
        totalCases: 3,
        passedCases: 3,
        failedCases: 0,
        skippedCases: 0,
        errorCases: 0,
        totalDuration: 200,
        cases: [
          { name: 'isEmpty_空字符串', suite: 'StringUtils测试', status: 'passed', duration: 50, timestamp: Date.now() },
          { name: 'isEmpty_非空字符串', suite: 'StringUtils测试', status: 'passed', duration: 60, timestamp: Date.now() },
          { name: 'capitalize_首字母大写', suite: 'StringUtils测试', status: 'passed', duration: 90, timestamp: Date.now() }
        ]
      }

      generator.addSuiteResult(suite)

      const json = generator.generateJson()
      const parsed = JSON.parse(json)
      assertEqual(parsed.projectName, 'MyApp')
      assertEqual(parsed.totalCases, 3)
      assertEqual(parsed.passedCases, 3)
    })

    it('generateHtml_生成HTML报告_包含关键信息', 0, () => {
      const suite: TestSuiteResult = {
        name: 'MathUtils测试',
        totalCases: 2,
        passedCases: 1,
        failedCases: 1,
        skippedCases: 0,
        errorCases: 0,
        totalDuration: 150,
        cases: [
          { name: 'add_正常', suite: 'MathUtils测试', status: 'passed', duration: 50, timestamp: Date.now() },
          { name: 'divide_异常', suite: 'MathUtils测试', status: 'failed', duration: 100, timestamp: Date.now(), errorMessage: '除数不能为零' }
        ]
      }

      generator.addSuiteResult(suite)

      const html = generator.generateHtml()
      assertContain(html, 'MyApp')
      assertContain(html, 'MathUtils测试')
      assertContain(html, '除数不能为零')
      assertContain(html, '50%')  // 通过率
    })

    it('generateMarkdown_生成Markdown报告_格式正确', 0, () => {
      const suite: TestSuiteResult = {
        name: 'PasswordValidator测试',
        totalCases: 4,
        passedCases: 3,
        failedCases: 1,
        skippedCases: 0,
        errorCases: 0,
        totalDuration: 300,
        cases: [
          { name: 'validate_合法密码', suite: 'PasswordValidator测试', status: 'passed', duration: 50, timestamp: Date.now() },
          { name: 'validate_密码过短', suite: 'PasswordValidator测试', status: 'passed', duration: 60, timestamp: Date.now() },
          { name: 'validate_缺少大写', suite: 'PasswordValidator测试', status: 'passed', duration: 70, timestamp: Date.now() },
          { name: 'validate_空密码', suite: 'PasswordValidator测试', status: 'failed', duration: 120, timestamp: Date.now(), errorMessage: 'Expected false but got true', expectedValue: 'false', actualValue: 'true' }
        ]
      }

      generator.addSuiteResult(suite)

      const md = generator.generateMarkdown()
      assertContain(md, 'MyApp')
      assertContain(md, 'PasswordValidator测试')
      assertContain(md, '75%')  // 通过率
      assertContain(md, 'Expected false but got true')
    })

    it('generateJUnitXml_生成XML报告_格式正确', 0, () => {
      const suite: TestSuiteResult = {
        name: 'DateUtils测试',
        totalCases: 2,
        passedCases: 2,
        failedCases: 0,
        skippedCases: 0,
        errorCases: 0,
        totalDuration: 100,
        cases: [
          { name: 'isLeapYear_闰年', suite: 'DateUtils测试', status: 'passed', duration: 40, timestamp: Date.now() },
          { name: 'isLeapYear_平年', suite: 'DateUtils测试', status: 'passed', duration: 60, timestamp: Date.now() }
        ]
      }

      generator.addSuiteResult(suite)

      const xml = generator.generateJUnitXml()
      assertContain(xml, '<?xml version="1.0"')
      assertContain(xml, '<testsuites')
      assertContain(xml, '<testsuite name="DateUtils测试"')
      assertContain(xml, '<testcase name="isLeapYear_闰年"')
    })
  })
}

3.3 完整示例:CI集成与报告发布

// CIReportPublisher.ets - CI报告发布器
import { TestReportGenerator, TestReport, TestSuiteResult } from './TestReportGenerator'

export interface CIConfig {
  reportDir: string
  failThreshold: number  // 通过率低于此值标记为失败
  historySize: number    // 保留历史报告数量
  notifyOnFailure: boolean
}

export class CIReportPublisher {
  private generator: TestReportGenerator
  private config: CIConfig
  private history: TestReport[] = []

  constructor(projectName: string, config: Partial<CIConfig> = {}) {
    this.generator = new TestReportGenerator(projectName, process.env.BUILD_NUMBER ?? 'local')
    this.config = {
      reportDir: config.reportDir ?? './reports',
      failThreshold: config.failThreshold ?? 80,
      historySize: config.historySize ?? 10,
      notifyOnFailure: config.notifyOnFailure ?? true
    }
  }

  addSuiteResult(suite: TestSuiteResult): void {
    this.generator.addSuiteResult(suite)
  }

  // 发布所有格式的报告
  async publishAll(): Promise<{ success: boolean; reportPath: string; passRate: number }> {
    const report = this.generator.getReport()
    const passRate = this.generator.getPassRate()

    // 保存到历史
    this.history.push(report)
    if (this.history.length > this.config.historySize) {
      this.history.shift()
    }

    // 生成各格式报告
    const jsonReport = this.generator.generateJson()
    const htmlReport = this.generator.generateHtml()
    const mdReport = this.generator.generateMarkdown()
    const xmlReport = this.generator.generateJUnitXml()

    // 输出文件路径(实际项目中写入文件系统)
    console.info(`报告目录: ${this.config.reportDir}`)
    console.info(`- JSON: ${this.config.reportDir}/report.json`)
    console.info(`- HTML: ${this.config.reportDir}/report.html`)
    console.info(`- Markdown: ${this.config.reportDir}/report.md`)
    console.info(`- JUnit XML: ${this.config.reportDir}/report.xml`)

    // 判断是否通过
    const success = passRate >= this.config.failThreshold

    // 失败通知
    if (!success && this.config.notifyOnFailure) {
      console.warn(`⚠️ 测试通过率 ${passRate}% 低于阈值 ${this.config.failThreshold}%`)
    }

    return {
      success,
      reportPath: this.config.reportDir,
      passRate
    }
  }

  // 获取趋势数据
  getTrend(): Array<{ build: string; passRate: number; totalCases: number; failedCases: number }> {
    return this.history.map(report => ({
      build: report.buildNumber,
      passRate: report.totalCases > 0
        ? Math.round((report.passedCases / report.totalCases) * 10000) / 100
        : 0,
      totalCases: report.totalCases,
      failedCases: report.failedCases
    }))
  }

  // 与上次对比
  compareToLast(): { passRateDiff: number; newFailures: string[]; fixedIssues: string[] } | null {
    if (this.history.length < 2) return null

    const current = this.history[this.history.length - 1]
    const previous = this.history[this.history.length - 2]

    const currentRate = current.totalCases > 0 ? (current.passedCases / current.totalCases) * 100 : 0
    const previousRate = previous.totalCases > 0 ? (previous.passedCases / previous.totalCases) * 100 : 0

    // 找出新增失败和修复的用例
    const currentFailures = new Set<string>()
    const previousFailures = new Set<string>()

    for (const suite of current.suites) {
      for (const testCase of suite.cases) {
        if (testCase.status === 'failed') {
          currentFailures.add(testCase.name)
        }
      }
    }

    for (const suite of previous.suites) {
      for (const testCase of suite.cases) {
        if (testCase.status === 'failed') {
          previousFailures.add(testCase.name)
        }
      }
    }

    const newFailures: string[] = []
    const fixedIssues: string[] = []

    currentFailures.forEach(name => {
      if (!previousFailures.has(name)) newFailures.push(name)
    })

    previousFailures.forEach(name => {
      if (!currentFailures.has(name)) fixedIssues.push(name)
    })

    return {
      passRateDiff: Math.round((currentRate - previousRate) * 100) / 100,
      newFailures,
      fixedIssues
    }
  }
}

四、踩坑与注意事项

坑点1:报告里只有"通过/失败"不够

Test passedTest failed——这种报告和没报告差不多。失败用例必须包含:错误消息、预期值、实际值、堆栈信息。没有这些信息,拿到报告的人还得去翻代码才能定位问题。

坑点2:超时用例的报告信息缺失

测试超时失败时,可能没有执行到断言,报告里只有"timeout"没有具体原因。在异步测试中,超时时记录当前执行到哪一步,方便排查是卡在哪里。

坑点3:报告文件路径不一致

CI环境中,测试报告的输出路径必须和CI工具配置的"报告收集路径"一致。路径对不上,CI工具找不到报告,等于白生成。在CI脚本中统一配置报告路径,测试代码和CI配置引用同一个变量

坑点4:历史数据没保留

只看当次报告,不知道趋势。上次5个失败这次3个失败,是进步了;上次0个失败这次3个失败,是退步了。保留历史报告数据,至少最近10次,才能看出趋势。

坑点5:大报告的生成性能

1000个测试用例的HTML报告可能好几MB,生成耗时也不短。如果CI对报告生成有时间限制,大项目可能超时。考虑增量报告——只报告本次变更相关的用例结果,或者分模块生成报告。

坑点6:跳过用例也要记录

@Skip跳过的用例不能"消失",必须在报告中体现。否则团队不知道哪些测试被跳过了、为什么被跳过了。跳过用例要标注原因,如@Skip('Bug #1234: 等待后端修复')

坑点7:报告中的敏感信息

测试数据中可能包含用户名、密码、Token等敏感信息,直接写进报告就泄露了。在报告生成时脱敏处理,把敏感值替换为***


五、HarmonyOS 6适配说明

API差异表

功能/接口 HarmonyOS 5 HarmonyOS 6 变更说明
报告格式 控制台文本 HTML/JSON/XML 多格式原生支持
报告生成 手动实现 @Report装饰器 声明式报告配置
CI集成 手动脚本 DevEco CI原生 一键集成
历史对比 内置趋势分析 自动对比历史
通知 飞书/企微/邮件 失败自动通知

行为变更

  1. 多格式报告原生支持:HarmonyOS 6的Hypium内置了HTML、JSON、XML三种报告格式,运行测试时通过--report html,json,xml参数指定,不再需要自己写报告生成器。

  2. @Report装饰器:可以在测试类上标注@Report({ format: 'html', output: './reports' }),声明式配置报告生成。

  3. 内置趋势分析:DevEco Studio 6的测试面板中可以直接查看历史趋势图,包括通过率变化、失败用例变化等。

  4. 失败通知集成:支持飞书、企业微信、邮件等通知渠道,测试失败时自动推送消息。

适配代码

// HarmonyOS 6声明式报告配置
import { describe, it, Report } from '@ohos/hypium'

@Report({
  format: ['html', 'json', 'xml'],
  output: './test-reports',
  includeHistory: true,
  notifyOnFailure: {
    channels: ['feishu', 'email'],
    recipients: ['team@example.com']
  }
})
describe('带报告配置的测试', () => {
  it('example_test', 0, () => {
    assertEqual(1 + 1, 2)
  })
})

// 命令行运行时指定报告格式
// hdc shell aa test -b com.example.app -m entry_test --report html,json --report-dir /data/reports

六、总结

维度 评价
学习难度 ⭐⭐
使用频率 ⭐⭐⭐⭐
重要程度 ⭐⭐⭐⭐

测试报告的核心价值就三个字:可操作。扫一眼知道整体状况,点进去定位失败原因,和历史对比看趋势变化。控制台日志不是报告,那只是"输出"。真正的报告应该是结构化的、可存档的、可对比的。HTML格式适合人看,JSON格式适合程序处理,JUnit XML格式适合CI工具——三种格式各有所长,按需选择。好的测试报告,是测试结果的"翻译官"——把技术细节翻译成决策依据

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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