HarmonyOS开发:测试报告与测试结果报告
HarmonyOS开发:测试报告与测试结果报告
📌 核心要点:测试跑完了,然后呢?一堆绿色红色你看得懂吗?测试报告是测试的"成绩单"——它不仅要告诉你"过了多少、挂了多少",还要告诉你"挂在哪里、为什么挂、怎么修"。学会生成、解读、定制测试报告,才能让测试结果真正指导开发决策。
一、背景与动机
你跑完100个测试用例,控制台刷了一屏日志,3个红的97个绿的。然后呢?你要花多长时间找到那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 passed或Test 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原生 | 一键集成 |
| 历史对比 | 无 | 内置趋势分析 | 自动对比历史 |
| 通知 | 无 | 飞书/企微/邮件 | 失败自动通知 |
行为变更
-
多格式报告原生支持:HarmonyOS 6的Hypium内置了HTML、JSON、XML三种报告格式,运行测试时通过
--report html,json,xml参数指定,不再需要自己写报告生成器。 -
@Report装饰器:可以在测试类上标注
@Report({ format: 'html', output: './reports' }),声明式配置报告生成。 -
内置趋势分析:DevEco Studio 6的测试面板中可以直接查看历史趋势图,包括通过率变化、失败用例变化等。
-
失败通知集成:支持飞书、企业微信、邮件等通知渠道,测试失败时自动推送消息。
适配代码
// 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工具——三种格式各有所长,按需选择。好的测试报告,是测试结果的"翻译官"——把技术细节翻译成决策依据。
- 点赞
- 收藏
- 关注作者
评论(0)