前置知识: HarmonyOS

测试与调试

00:00
5 min Intermediate 2026/6/14

单元测试与调试技巧

概述

测试与调试是保证应用质量的关键环节。HarmonyOS 提供了多种测试框架和调试工具,包括单元测试框架、UI 测试框架、DevEco Studio 的调试器和 Profiler 性能分析工具。掌握这些工具的使用,能帮助你快速定位和修复问题,提高开发效率。

为什么需要测试?没有测试的代码就像没有安全网的走钢丝。当你修改代码时,无法确定是否引入了新的 Bug。测试用例是代码的安全网,让你有信心进行重构和功能迭代。调试则是定位问题的利器,当应用行为与预期不符时,调试器能帮你找到根本原因。

基础概念

单元测试:针对单个函数或类的测试,验证其行为是否符合预期。运行速度快,不依赖 UI 和设备环境。

UI 测试:模拟用户操作(点击、滑动、输入)来验证界面行为。运行速度较慢,需要在设备或模拟器上运行。

断言:测试中的验证语句,如 assertEqualassertTrue 等。断言失败表示测试不通过。

调试器:允许你在代码中设置断点,暂停程序执行,查看变量值和调用栈。

日志:通过 console.infoconsole.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 中使用调试器:

  1. 在代码行号左侧点击,设置断点(红点标记)
  2. 点击工具栏的 “Debug” 按钮(虫子图标)启动调试
  3. 程序执行到断点时会暂停
  4. 在调试面板中可以:
    • 查看变量值
    • 查看调用栈
    • 单步执行(Step Over、Step Into、Step Out)
    • 在表达式窗口中计算表达式
  5. 使用条件断点:右键断点,设置触发条件

性能分析

使用 DevEco Profiler 分析性能:

  1. 选择 View -> Tool Windows -> Profiler
  2. 选择分析类型:
    • CPU:分析函数执行时间和调用频率
    • Memory:分析内存分配和泄漏
    • Network:分析网络请求
  3. 点击录制按钮开始分析
  4. 操作应用复现性能问题
  5. 停止录制,查看分析结果

远程调试

在真机上调试:

  1. 用 USB 连接设备到电脑
  2. 在设备上开启开发者模式和 USB 调试
  3. 在 DevEco Studio 中选择连接的设备
  4. 击 Debug 按钮启动调试

无线调试

  1. 确保设备和电脑在同一网络
  2. 设备启用无线调试
  3. 使用设备的 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/

查看覆盖率报告可以了解哪些代码没有被测试覆盖帮助补充测试用例

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式