测试与调试
单元测试与调试技巧
概述
测试与调试是保证应用质量的关键环节。HarmonyOS 提供了多种测试框架和调试工具,包括单元测试框架、UI 测试框架、DevEco Studio 的调试器和 Profiler 性能分析工具。掌握这些工具的使用,能帮助你快速定位和修复问题,提高开发效率。
为什么需要测试?没有测试的代码就像没有安全网的走钢丝。当你修改代码时,无法确定是否引入了新的 Bug。测试用例是代码的安全网,让你有信心进行重构和功能迭代。调试则是定位问题的利器,当应用行为与预期不符时,调试器能帮你找到根本原因。
基础概念
单元测试:针对单个函数或类的测试,验证其行为是否符合预期。运行速度快,不依赖 UI 和设备环境。
UI 测试:模拟用户操作(点击、滑动、输入)来验证界面行为。运行速度较慢,需要在设备或模拟器上运行。
断言:测试中的验证语句,如 assertEqual、assertTrue 等。断言失败表示测试不通过。
调试器:允许你在代码中设置断点,暂停程序执行,查看变量值和调用栈。
日志:通过 console.info、console.error 等方法输出运行时信息,是最基本的调试手段。
快速上手
单元测试
HarmonyOS 使用 Jest 作为单元测试框架:
// 被测试的函数
// src/main/ets/utils/Calculator.ets
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
}
// 测试文件
// src/test/ets/utils/Calculator.test.ets
import { add, multiply, divide } from '../../../main/ets/utils/Calculator';
// describe 分组,it 定义测试用例
describe('Calculator', () => {
// 测试加法
it('应该正确计算两数之和', () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
// 测试乘法
it('应该正确计算两数之积', () => {
expect(multiply(2, 3)).toBe(6);
expect(multiply(-2, 3)).toBe(-6);
expect(multiply(0, 100)).toBe(0);
});
// 测试异常情况
it('除数为零时应该抛出异常', () => {
expect(() => divide(1, 0)).toThrow('除数不能为零');
});
});
运行测试:
# 运行所有测试
hvigorw test
# 运行指定测试文件
hvigorw test --test-file Calculator.test.ets
日志调试
import hilog from '@ohos.hilog'
const TAG = 'MyApp'
const DOMAIN = 0x0001 // 日志域,用于过滤
@Entry
@Component
struct LogDemo {
aboutToAppear() {
// 不同级别的日志
hilog.info(DOMAIN, TAG, '应用启动') // 信息
hilog.warn(DOMAIN, TAG, '配置缺失,使用默认值') // 警告
hilog.error(DOMAIN, TAG, '数据加载失败') // 错误
hilog.debug(DOMAIN, TAG, '调试信息: count=%{public}d', 42) // 调试
}
build() {
Column() {
Text('日志调试示例')
}
}
}
详细用法
更多断言方法
describe('断言示例', () => {
it('相等性断言', () => {
// 严格相等
expect(1 + 1).toBe(2);
expect('hello').toBe('hello');
// 深度相等(对象内容比较)
expect({ name: '张三' }).toEqual({ name: '张三' });
expect([1, 2, 3]).toEqual([1, 2, 3]);
});
it('布尔断言', () => {
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(1).toBeDefined();
});
it('数字比较', () => {
expect(5).toBeGreaterThan(3);
expect(3).toBeLessThan(5);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // 浮点数近似比较
});
it('字符串匹配', () => {
expect('Hello World').toContain('World');
expect('Hello World').toMatch(/World/);
});
it('数组和集合', () => {
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
});
it('异常断言', () => {
expect(() => {
throw new Error('出错了');
}).toThrow('出错了');
});
it('异步测试', async () => {
const result = await fetchData();
expect(result).toBe('数据');
});
});
测试生命周期
describe('生命周期示例', () => {
let counter: number;
// 每个测试用例之前执行
beforeEach(() => {
counter = 0;
});
// 每个测试用例之后执行
afterEach(() => {
// 清理操作
});
// 所有测试用例之前执行一次
beforeAll(() => {
// 初始化共享资源
});
// 所有测试用例之后执行一次
afterAll(() => {
// 释放共享资源
});
it('计数器初始值为0', () => {
expect(counter).toBe(0);
});
it('计数器加1', () => {
counter++;
expect(counter).toBe(1);
});
it('每个测试开始时计数器重置', () => {
// 因为 beforeEach 会重置 counter
expect(counter).toBe(0);
});
});
Mock 函数
// 模拟依赖服务
class UserService {
private api: ApiClient;
constructor(api: ApiClient) {
this.api = api;
}
async getUser(id: number): Promise<User> {
const response = await this.api.get(`/users/${id}`);
return response.data;
}
}
// 测试时使用 Mock
describe('UserService', () => {
it('应该正确获取用户信息', async () => {
// 创建 Mock 对象
const mockApi = {
get: jest.fn().mockResolvedValue({
data: { id: 1, name: '张三' },
}),
};
const service = new UserService(mockApi as any);
const user = await service.getUser(1);
// 验证返回值
expect(user.name).toBe('张三');
// 验证 Mock 被正确调用
expect(mockApi.get).toHaveBeenCalledWith('/users/1');
expect(mockApi.get).toHaveBeenCalledTimes(1);
});
});
使用调试器
在 DevEco Studio 中使用调试器:
- 在代码行号左侧点击,设置断点(红点标记)
- 点击工具栏的 “Debug” 按钮(虫子图标)启动调试
- 程序执行到断点时会暂停
- 在调试面板中可以:
- 查看变量值
- 查看调用栈
- 单步执行(Step Over、Step Into、Step Out)
- 在表达式窗口中计算表达式
- 使用条件断点:右键断点,设置触发条件
性能分析
使用 DevEco Profiler 分析性能:
- 选择 View -> Tool Windows -> Profiler
- 选择分析类型:
- CPU:分析函数执行时间和调用频率
- Memory:分析内存分配和泄漏
- Network:分析网络请求
- 点击录制按钮开始分析
- 操作应用复现性能问题
- 停止录制,查看分析结果
远程调试
在真机上调试:
- 用 USB 连接设备到电脑
- 在设备上开启开发者模式和 USB 调试
- 在 DevEco Studio 中选择连接的设备
- 点击 Debug 按钮启动调试
无线调试:
- 确保设备和电脑在同一网络
- 在设备上启用无线调试
- 使用设备的 IP 地址和端口连接
常见场景
测试自定义组件
// 被测试的组件
@Component
export struct Counter {
@State count: number = 0
increment() {
this.count++
}
decrement() {
this.count--
}
reset() {
this.count = 0
}
build() {
Column() {
Text(`计数: ${this.count}`)
Button('加1').onClick(() => this.increment())
Button('减1').onClick(() => this.decrement())
}
}
}
// 测试组件逻辑(不涉及 UI 渲染)
describe('Counter 逻辑', () => {
it('increment 应该增加计数', () => {
const counter = new Counter()
counter.increment()
expect(counter.count).toBe(1)
counter.increment()
expect(counter.count).toBe(2)
})
it('decrement 应该减少计数', () => {
const counter = new Counter()
counter.decrement()
expect(counter.count).toBe(-1)
})
it('reset 应该重置计数', () => {
const counter = new Counter()
counter.increment()
counter.increment()
counter.reset()
expect(counter.count).toBe(0)
})
})
调试网络请求
import http from '@ohos.net.http';
async function debugNetworkRequest() {
const httpRequest = http.createHttp();
try {
hilog.info(DOMAIN, TAG, '开始发起请求');
const response = await httpRequest.request('https://api.example.com/data', {
method: http.RequestMethod.GET,
});
// 记录响应信息
hilog.info(DOMAIN, TAG, `响应码: ${response.responseCode}`);
hilog.info(DOMAIN, TAG, `响应头: ${JSON.stringify(response.header)}`);
hilog.debug(DOMAIN, TAG, `响应体: ${response.result}`);
} catch (error) {
hilog.error(DOMAIN, TAG, `请求异常: ${error}`);
} finally {
httpRequest.destroy();
}
}
注意事项
测试文件位置:单元测试文件放在 src/test/ets/ 目录下,UI 测试文件放在 src/ohosTest/ets/ 目录下。
测试命名:测试文件以 .test.ets 结尾,测试函数名应清晰描述测试意图。
测试独立性:每个测试用例应该独立运行,不依赖其他测试用例的执行顺序或状态。
日志级别:生产环境不要输出 debug 级别的日志,避免泄露敏感信息。使用 %{public} 标记公开信息。
断点调试性能:调试模式下应用运行速度会变慢,不要以调试模式的性能数据为依据进行优化。
进阶用法
参数化测试
describe('参数化测试示例', () => {
// 使用 test.each 进行参数化测试
const testCases = [
{ a: 1, b: 2, expected: 3 },
{ a: -1, b: 1, expected: 0 },
{ a: 0, b: 0, expected: 0 },
{ a: 100, b: 200, expected: 300 },
];
testCases.forEach(({ a, b, expected }) => {
it(`${a} + ${b} 应该等于 ${expected}`, () => {
expect(add(a, b)).toBe(expected);
});
});
});
测试覆盖率
# 运行测试并生成覆盖率报告
hvigorw test --coverage
# 覆盖率报告位于
# entry/build/test/coverage/
查看覆盖率报告可以了解哪些代码没有被测试覆盖,帮助补充测试用例。