ArkTS 提供了完善的单元测试框架,支持对 HarmonyOS 应用进行单元测试。以下是完整的 ArkTS 单元测试指南:
1. 测试环境配置
1.1 项目结构
css
project/
├── src/
│ └── main/
│ └── ets/
│ └── ...
├── ohosTest/
│ └── src/
│ └── test/
│ └── ets/
│ └── test/
│ └── Example.test.ets
│ └── TestAbility.ts
│ └── resources/
│ └── module.json5
1.2 module.json5 配置
json
// ohosTest/src/module.json5
{
"module": {
"name": "test",
"type": "feature",
"srcEntrance": "./ets/TestAbility.ts",
"description": "$string:TestAbility_desc",
"mainElement": "TestAbility",
"deviceTypes": [
"phone",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:test_pages",
"abilities": [
{
"name": "TestAbility",
"srcEntrance": "./ets/TestAbility.ts",
"description": "$string:TestAbility_desc",
"icon": "$media:icon",
"label": "$string:TestAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"visible": true,
"skills": [
{
"actions": [
"action.system.home"
],
"entities": [
"entity.system.home"
]
}
]
}
]
}
}
2. 基础单元测试
2.1 工具类测试
typescript
// src/main/ets/utils/MathUtil.ets
export class MathUtil {
static add(a: number, b: number): number {
return a + b;
}
static subtract(a: number, b: number): number {
return a - b;
}
static multiply(a: number, b: number): number {
return a * b;
}
static divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
static isEven(num: number): boolean {
return num % 2 === 0;
}
static factorial(n: number): number {
if (n < 0) throw new Error('Negative number');
if (n === 0 || n === 1) return 1;
return n * this.factorial(n - 1);
}
}
typescript
// ohosTest/src/test/ets/test/MathUtil.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { MathUtil } from '../../../src/main/ets/utils/MathUtil';
export default function mathUtilTest() {
describe('MathUtil Tests', () => {
it('should_add_two_numbers', 0, () => {
const result = MathUtil.add(2, 3);
expect(result).assertEqual(5);
});
it('should_subtract_two_numbers', 0, () => {
const result = MathUtil.subtract(5, 3);
expect(result).assertEqual(2);
});
it('should_multiply_two_numbers', 0, () => {
const result = MathUtil.multiply(4, 3);
expect(result).assertEqual(12);
});
it('should_divide_two_numbers', 0, () => {
const result = MathUtil.divide(10, 2);
expect(result).assertEqual(5);
});
it('should_throw_error_when_dividing_by_zero', 0, () => {
try {
MathUtil.divide(10, 0);
expect(true).assertFalse(); // 不应该执行到这里
} catch (error) {
expect(error.message).assertEqual('Division by zero');
}
});
it('should_detect_even_numbers', 0, () => {
expect(MathUtil.isEven(4)).assertTrue();
expect(MathUtil.isEven(7)).assertFalse();
});
it('should_calculate_factorial', 0, () => {
expect(MathUtil.factorial(5)).assertEqual(120);
expect(MathUtil.factorial(0)).assertEqual(1);
});
it('should_throw_error_for_negative_factorial', 0, () => {
try {
MathUtil.factorial(-1);
expect(true).assertFalse();
} catch (error) {
expect(error.message).assertEqual('Negative number');
}
});
});
}
2.2 业务逻辑测试
typescript
// src/main/ets/services/UserService.ets
export class UserService {
private users: Map<string, User> = new Map();
addUser(user: User): boolean {
if (this.users.has(user.id)) {
return false;
}
this.users.set(user.id, user);
return true;
}
getUser(id: string): User | undefined {
return this.users.get(id);
}
deleteUser(id: string): boolean {
return this.users.delete(id);
}
getAllUsers(): User[] {
return Array.from(this.users.values());
}
updateUser(user: User): boolean {
if (!this.users.has(user.id)) {
return false;
}
this.users.set(user.id, user);
return true;
}
}
export class User {
constructor(
public id: string,
public name: string,
public email: string,
public age: number
) {}
}
typescript
// ohosTest/src/test/ets/test/UserService.test.ets
import { describe, it, expect, beforeEach } from '@ohos/hypium';
import { UserService, User } from '../../../src/main/ets/services/UserService';
export default function userServiceTest() {
describe('UserService Tests', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
});
it('should_add_user_successfully', 0, () => {
const user = new User('1', 'John Doe', 'john@example.com', 30);
const result = userService.addUser(user);
expect(result).assertTrue();
expect(userService.getUser('1')).assertEqual(user);
});
it('should_not_add_duplicate_user', 0, () => {
const user = new User('1', 'John Doe', 'john@example.com', 30);
userService.addUser(user);
const result = userService.addUser(user);
expect(result).assertFalse();
});
it('should_get_user_by_id', 0, () => {
const user = new User('1', 'John Doe', 'john@example.com', 30);
userService.addUser(user);
const retrievedUser = userService.getUser('1');
expect(retrievedUser).assertEqual(user);
});
it('should_return_undefined_for_nonexistent_user', 0, () => {
const retrievedUser = userService.getUser('nonexistent');
expect(retrievedUser).assertUndefined();
});
it('should_delete_user', 0, () => {
const user = new User('1', 'John Doe', 'john@example.com', 30);
userService.addUser(user);
const deleteResult = userService.deleteUser('1');
expect(deleteResult).assertTrue();
expect(userService.getUser('1')).assertUndefined();
});
it('should_return_false_when_deleting_nonexistent_user', 0, () => {
const deleteResult = userService.deleteUser('nonexistent');
expect(deleteResult).assertFalse();
});
it('should_get_all_users', 0, () => {
const user1 = new User('1', 'John Doe', 'john@example.com', 30);
const user2 = new User('2', 'Jane Smith', 'jane@example.com', 25);
userService.addUser(user1);
userService.addUser(user2);
const allUsers = userService.getAllUsers();
expect(allUsers.length).assertEqual(2);
expect(allUsers).assertContain(user1);
expect(allUsers).assertContain(user2);
});
it('should_update_user', 0, () => {
const user = new User('1', 'John Doe', 'john@example.com', 30);
userService.addUser(user);
const updatedUser = new User('1', 'John Updated', 'updated@example.com', 31);
const updateResult = userService.updateUser(updatedUser);
expect(updateResult).assertTrue();
expect(userService.getUser('1')).assertEqual(updatedUser);
});
it('should_return_false_when_updating_nonexistent_user', 0, () => {
const user = new User('1', 'John Doe', 'john@example.com', 30);
const updateResult = userService.updateUser(user);
expect(updateResult).assertFalse();
});
});
}
3. 异步测试
3.1 异步服务测试
typescript
// src/main/ets/services/ApiService.ets
export class ApiService {
async fetchData(url: string): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.startsWith('https://')) {
resolve(`Data from ${url}`);
} else {
reject(new Error('Invalid URL'));
}
}, 100);
});
}
async processUserData(userId: string): Promise<UserData> {
// 模拟 API 调用
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: userId,
name: `User ${userId}`,
score: Math.random() * 100
});
}, 50);
});
}
}
export interface UserData {
id: string;
name: string;
score: number;
}
typescript
// ohosTest/src/test/ets/test/ApiService.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { ApiService, UserData } from '../../../src/main/ets/services/ApiService';
export default function apiServiceTest() {
describe('ApiService Tests', () => {
const apiService = new ApiService();
it('should_fetch_data_successfully', 0, async () => {
const result = await apiService.fetchData('https://api.example.com/data');
expect(result).assertEqual('Data from https://api.example.com/data');
});
it('should_reject_invalid_url', 0, async () => {
try {
await apiService.fetchData('invalid-url');
expect(true).assertFalse(); // 不应该执行到这里
} catch (error) {
expect(error.message).assertEqual('Invalid URL');
}
});
it('should_process_user_data', 0, async () => {
const userData: UserData = await apiService.processUserData('123');
expect(userData.id).assertEqual('123');
expect(userData.name).assertEqual('User 123');
expect(userData.score).assertLarger(0);
expect(userData.score).assertLess(100);
});
it('should_process_multiple_users', 0, async () => {
const promises = [
apiService.processUserData('1'),
apiService.processUserData('2'),
apiService.processUserData('3')
];
const results = await Promise.all(promises);
expect(results.length).assertEqual(3);
results.forEach((userData, index) => {
expect(userData.id).assertEqual((index + 1).toString());
expect(userData.name).assertEqual(`User ${index + 1}`);
});
});
});
}
4. 组件测试
4.1 自定义组件测试
typescript
// src/main/ets/components/CounterComponent.ets
@Component
export struct CounterComponent {
@State count: number = 0;
private maxCount: number = 10;
build() {
Column() {
Text(`Count: ${this.count}`)
.fontSize(20)
.fontColor(this.count >= this.maxCount ? Color.Red : Color.Black)
Button('Increment')
.onClick(() => {
if (this.count < this.maxCount) {
this.count++;
}
})
.enabled(this.count < this.maxCount)
Button('Reset')
.onClick(() => {
this.count = 0;
})
}
}
// 公共方法用于测试
increment(): void {
if (this.count < this.maxCount) {
this.count++;
}
}
reset(): void {
this.count = 0;
}
getCount(): number {
return this.count;
}
isMaxReached(): boolean {
return this.count >= this.maxCount;
}
}
typescript
// ohosTest/src/test/ets/test/CounterComponent.test.ets
import { describe, it, expect, beforeEach } from '@ohos/hypium';
import { CounterComponent } from '../../../src/main/ets/components/CounterComponent';
export default function counterComponentTest() {
describe('CounterComponent Tests', () => {
let counter: CounterComponent;
beforeEach(() => {
counter = new CounterComponent();
});
it('should_initialize_with_zero', 0, () => {
expect(counter.getCount()).assertEqual(0);
expect(counter.isMaxReached()).assertFalse();
});
it('should_increment_count', 0, () => {
counter.increment();
expect(counter.getCount()).assertEqual(1);
});
it('should_not_exceed_max_count', 0, () => {
for (let i = 0; i < 15; i++) {
counter.increment();
}
expect(counter.getCount()).assertEqual(10);
expect(counter.isMaxReached()).assertTrue();
});
it('should_reset_count', 0, () => {
counter.increment();
counter.increment();
expect(counter.getCount()).assertEqual(2);
counter.reset();
expect(counter.getCount()).assertEqual(0);
expect(counter.isMaxReached()).assertFalse();
});
it('should_handle_multiple_operations', 0, () => {
// 增加 5 次
for (let i = 0; i < 5; i++) {
counter.increment();
}
expect(counter.getCount()).assertEqual(5);
// 重置
counter.reset();
expect(counter.getCount()).assertEqual(0);
// 再次增加
counter.increment();
expect(counter.getCount()).assertEqual(1);
});
});
}
5. Mock 和 Stub 测试
5.1 依赖注入测试
typescript
// src/main/ets/services/WeatherService.ets
export class WeatherService {
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async getWeather(city: string): Promise<WeatherData> {
// 实际应用中这里会调用真实的 API
throw new Error('Not implemented in tests');
}
}
export interface WeatherData {
city: string;
temperature: number;
description: string;
}
// 模拟实现用于测试
export class MockWeatherService extends WeatherService {
constructor() {
super('test-key');
}
override async getWeather(city: string): Promise<WeatherData> {
return Promise.resolve({
city: city,
temperature: 25,
description: 'Sunny'
});
}
}
typescript
// ohosTest/src/test/ets/test/WeatherService.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { WeatherService, MockWeatherService, WeatherData } from '../../../src/main/ets/services/WeatherService';
export default function weatherServiceTest() {
describe('WeatherService Tests', () => {
it('should_create_weather_service_with_api_key', 0, () => {
const weatherService = new WeatherService('real-api-key');
// 可以验证构造函数逻辑
expect(weatherService).not().assertUndefined();
});
it('should_get_weather_data_using_mock', 0, async () => {
const mockService = new MockWeatherService();
const weatherData: WeatherData = await mockService.getWeather('Beijing');
expect(weatherData.city).assertEqual('Beijing');
expect(weatherData.temperature).assertEqual(25);
expect(weatherData.description).assertEqual('Sunny');
});
it('should_handle_multiple_cities_with_mock', 0, async () => {
const mockService = new MockWeatherService();
const cities = ['Beijing', 'Shanghai', 'Guangzhou'];
const promises = cities.map(city => mockService.getWeather(city));
const results = await Promise.all(promises);
expect(results.length).assertEqual(3);
results.forEach((weatherData, index) => {
expect(weatherData.city).assertEqual(cities[index]);
expect(weatherData.temperature).assertEqual(25);
});
});
});
}
6. 测试运行配置
6.1 测试列表文件
typescript
// ohosTest/src/test/ets/test/TestList.test.ets
import mathUtilTest from './MathUtil.test.ets';
import userServiceTest from './UserService.test.ets';
import apiServiceTest from './ApiService.test.ets';
import counterComponentTest from './CounterComponent.test.ets';
import weatherServiceTest from './WeatherService.test.ets';
export default function testList() {
mathUtilTest();
userServiceTest();
apiServiceTest();
counterComponentTest();
weatherServiceTest();
}
6.2 运行测试
bash
# 在项目根目录运行
./gradlew hmosTest
# 或者
npm test
7. 测试最佳实践
7.1 测试命名规范
- 测试方法名应该描述性很强
- 使用
should_前缀描述预期行为 - 测试用例应该独立,不依赖其他测试
7.2 测试组织结构
- 每个被测试类对应一个测试文件
- 使用
describe块组织相关测试 - 使用
beforeEach进行测试准备
7.3 断言使用
- 使用明确的断言方法
- 一个测试用例一个断言(理想情况)
- 测试边界条件和异常情况
这样完整的单元测试框架可以确保 ArkTS 代码的质量和可靠性,支持 TDD(测试驱动开发)实践。