ArkTs单元测试 UnitTest 指南

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(测试驱动开发)实践。

相关推荐
LXA08093 小时前
vue3开发使用框架推荐
前端·javascript·vue.js
拿不拿铁193 小时前
Vite & Webpack & Rollup 入口与产出配置与示例
前端
用户90443816324603 小时前
React 5 个 “隐形坑”:上线前没注意,debug 到凌晨 3 点
前端·javascript·react.js
StarkCoder3 小时前
打造炫酷浮动式 TabBar:让 iOS 应用导航更有格调!
前端·ios
AAA阿giao3 小时前
Promise:让 JavaScript 异步任务“同步化”的利器
前端·javascript·promise
光影少年3 小时前
vite7更新了哪些内容
前端
六月的可乐3 小时前
前端自定义右键菜单与图片复制(兼容H5)
前端
浮游本尊4 小时前
React 18.x 学习计划 - 第八天:React测试
前端·学习·react.js
麦麦在写代码4 小时前
前端学习1
前端·学习