HarmonyOS鸿蒙Next应用开发中如何编写单元测试?

HarmonyOS鸿蒙Next应用开发中如何编写单元测试? 1.在HarmonyOS应用开发中如何编写单元测试?
2.如何进行UI自动化测试和代码质量保障?

3 回复

解决方案

1. 基础单元测试

// tests/ExampleTest.test.ets
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '[@ohos](/user/ohos)/hypium'

export default function exampleTest() {
  describe('基础单元测试', () => {
    // 所有测试前执行一次
    beforeAll(() => {
      console.log('测试套件开始')
    })

    // 所有测试后执行一次
    afterAll(() => {
      console.log('测试套件结束')
    })

    // 每个测试前执行
    beforeEach(() => {
      console.log('测试用例开始')
    })

    // 每个测试后执行
    afterEach(() => {
      console.log('测试用例结束')
    })

    // 测试用例
    it('should_add_two_numbers', () => {
      const result = add(2, 3)
      expect(result).assertEqual(5)
    })

    it('should_multiply_two_numbers', () => {
      const result = multiply(4, 5)
      expect(result).assertEqual(20)
    })

    it('should_throw_error_for_division_by_zero', () => {
      expect(() => {
        divide(10, 0)
      }).assertThrow('Cannot divide by zero')
    })
  })
}

// 被测试的函数
function add(a: number, b: number): number {
  return a + b
}

function multiply(a: number, b: number): number {
  return a * b
}

function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('Cannot divide by zero')
  }
  return a / b
}

2. 工具类测试

// utils/StringUtils.ets
export class StringUtils {
  static isEmpty(str: string | null | undefined): boolean {
    return str === null || str === undefined || str.trim() === ''
  }

  static capitalize(str: string): string {
    if (this.isEmpty(str)) return ''
    return str.charAt(0).toUpperCase() + str.slice(1)
  }

  static truncate(str: string, length: number): string {
    if (str.length <= length) return str
    return str.substring(0, length) + '...'
  }

  static reverse(str: string): string {
    return str.split('').reverse().join('')
  }
}

// tests/StringUtils.test.ets
import { describe, it, expect } from '[@ohos](/user/ohos)/hypium'
import { StringUtils } from '../utils/StringUtils'

export default function stringUtilsTest() {
  describe('StringUtils测试', () => {
    describe('isEmpty方法', () => {
      it('should_return_true_for_null', () => {
        expect(StringUtils.isEmpty(null)).assertTrue()
      })

      it('should_return_true_for_undefined', () => {
        expect(StringUtils.isEmpty(undefined)).assertTrue()
      })

      it('should_return_true_for_empty_string', () => {
        expect(StringUtils.isEmpty('')).assertTrue()
      })

      it('should_return_true_for_whitespace', () => {
        expect(StringUtils.isEmpty('   ')).assertTrue()
      })

      it('should_return_false_for_valid_string', () => {
        expect(StringUtils.isEmpty('hello')).assertFalse()
      })
    })

    describe('capitalize方法', () => {
      it('should_capitalize_first_letter', () => {
        expect(StringUtils.capitalize('hello')).assertEqual('Hello')
      })

      it('should_return_empty_for_empty_string', () => {
        expect(StringUtils.capitalize('')).assertEqual('')
      })

      it('should_handle_single_char', () => {
        expect(StringUtils.capitalize('a')).assertEqual('A')
      })
    })

    describe('truncate方法', () => {
      it('should_truncate_long_string', () => {
        const result = StringUtils.truncate('Hello World', 5)
        expect(result).assertEqual('Hello...')
      })

      it('should_not_truncate_short_string', () => {
        const result = StringUtils.truncate('Hi', 5)
        expect(result).assertEqual('Hi')
      })
    })

    describe('reverse方法', () => {
      it('should_reverse_string', () => {
        expect(StringUtils.reverse('hello')).assertEqual('olleh')
      })

      it('should_handle_empty_string', () => {
        expect(StringUtils.reverse('')).assertEqual('')
      })
    })
  })
}

3. ViewModel测试

// viewmodel/UserViewModel.ets
export class UserViewModel {
  private username: string = ''
  private email: string = ''

  setUsername(value: string): void {
    this.username = value
  }

  setEmail(value: string): void {
    this.email = value
  }

  getUsername(): string {
    return this.username
  }

  getEmail(): string {
    return this.email
  }

  validate(): { valid: boolean, errors: string[] } {
    const errors: string[] = []

    if (!this.username || this.username.length < 3) {
      errors.push('用户名至少3个字符')
    }

    if (!this.email || !this.isValidEmail(this.email)) {
      errors.push('邮箱格式不正确')
    }

    return {
      valid: errors.length === 0,
      errors: errors
    }
  }

  private isValidEmail(email: string): boolean {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return regex.test(email)
  }

  clear(): void {
    this.username = ''
    this.email = ''
  }
}

// tests/UserViewModel.test.ets
import { describe, it, expect, beforeEach } from '[@ohos](/user/ohos)/hypium'
import { UserViewModel } from '../viewmodel/UserViewModel'

export default function userViewModelTest() {
  describe('UserViewModel测试', () => {
    let viewModel: UserViewModel

    beforeEach(() => {
      viewModel = new UserViewModel()
    })

    describe('设置和获取用户名', () => {
      it('should_set_and_get_username', () => {
        viewModel.setUsername('testuser')
        expect(viewModel.getUsername()).assertEqual('testuser')
      })
    })

    describe('设置和获取邮箱', () => {
      it('should_set_and_get_email', () => {
        viewModel.setEmail('test@example.com')
        expect(viewModel.getEmail()).assertEqual('test@example.com')
      })
    })

    describe('数据验证', () => {
      it('should_fail_validation_for_short_username', () => {
        viewModel.setUsername('ab')
        viewModel.setEmail('test@example.com')

        const result = viewModel.validate()
        expect(result.valid).assertFalse()
        expect(result.errors.length).assertEqual(1)
        expect(result.errors[0]).assertEqual('用户名至少3个字符')
      })

      it('should_fail_validation_for_invalid_email', () => {
        viewModel.setUsername('testuser')
        viewModel.setEmail('invalid-email')

        const result = viewModel.validate()
        expect(result.valid).assertFalse()
        expect(result.errors.length).assertEqual(1)
        expect(result.errors[0]).assertEqual('邮箱格式不正确')
      })

      it('should_pass_validation_for_valid_data', () => {
        viewModel.setUsername('testuser')
        viewModel.setEmail('test@example.com')

        const result = viewModel.validate()
        expect(result.valid).assertTrue()
        expect(result.errors.length).assertEqual(0)
      })
    })

    describe('清空数据', () => {
      it('should_clear_all_data', () => {
        viewModel.setUsername('testuser')
        viewModel.setEmail('test@example.com')
        viewModel.clear()

        expect(viewModel.getUsername()).assertEqual('')
        expect(viewModel.getEmail()).assertEqual('')
      })
    })
  })
}

4. Mock数据测试

// service/UserService.ets
import http from '[@ohos](/user/ohos).net.http'

export interface User {
  id: number
  name: string
  email: string
}

export class UserService {
  private baseUrl = 'https://api.example.com'

  async getUser(id: number): Promise<User> {
    const httpRequest = http.createHttp()
    
    try {
      const response = await httpRequest.request(`${this.baseUrl}/users/${id}`)
      return JSON.parse(response.result as string) as User
    } finally {
      httpRequest.destroy()
    }
  }

  async createUser(user: Omit<User, 'id'>): Promise<User> {
    const httpRequest = http.createHttp()
    
    try {
      const response = await httpRequest.request(
        `${this.baseUrl}/users`,
        {
          method: http.RequestMethod.POST,
          extraData: JSON.stringify(user)
        }
      )
      return JSON.parse(response.result as string) as User
    } finally {
      httpRequest.destroy()
    }
  }
}

// tests/UserService.test.ets
import { describe, it, expect, beforeEach } from '[@ohos](/user/ohos)/hypium'
import { UserService, User } from '../service/UserService'

// Mock数据
const mockUser: User = {
  id: 1,
  name: 'Test User',
  email: 'test@example.com'
}

export default function userServiceTest() {
  describe('UserService测试', () => {
    let service: UserService

    beforeEach(() => {
      service = new UserService()
    })

    // 注意: 实际测试需要Mock网络请求
    // 这里仅作示例,实际应使用Mock框架

    it('should_get_user_by_id', async () => {
      // 在实际测试中,应Mock http请求
      // 这里假设已经Mock了响应
      
      // const user = await service.getUser(1)
      // expect(user.id).assertEqual(mockUser.id)
      // expect(user.name).assertEqual(mockUser.name)
      // expect(user.email).assertEqual(mockUser.email)
    })

    it('should_create_user', async () => {
      // const newUser = {
      //   name: 'New User',
      //   email: 'new@example.com'
      // }
      
      // const created = await service.createUser(newUser)
      // expect(created.id).assertLarger(0)
      // expect(created.name).assertEqual(newUser.name)
      // expect(created.email).assertEqual(newUser.email)
    })
  })
}

5. 测试工具类

// tests/TestUtil.ets
export class TestUtil {
  /**
   * 创建Mock数据
   */
  static createMockArray<T>(count: number, factory: (index: number) => T): T[] {
    return Array.from({ length: count }, (_, i) => factory(i))
  }

  /**
   * 等待异步完成
   */
  static async waitFor(
    condition: () => boolean,
    timeout: number = 5000,
    interval: number = 100
  ): Promise<void> {
    const startTime = Date.now()

    while (!condition()) {
      if (Date.now() - startTime > timeout) {
        throw new Error('等待超时')
      }
      await this.delay(interval)
    }
  }

  /**
   * 延时
   */
  static delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms))
  }

  /**
   * 断言数组包含
   */
  static assertArrayContains<T>(array: T[], item: T): void {
    if (!array.includes(item)) {
      throw new Error(`数组不包含元素: ${item}`)
    }
  }

  /**
   * 断言对象相等
   */
  static assertObjectEquals(obj1: Object, obj2: Object): void {
    const json1 = JSON.stringify(obj1)
    const json2 = JSON.stringify(obj2)
    
    if (json1 !== json2) {
      throw new Error(`对象不相等:\n期望: ${json2}\n实际: ${json1}`)
    }
  }

  /**
   * 生成随机字符串
   */
  static randomString(length: number = 10): string {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    let result = ''
    for (let i = 0; i < length; i++) {
      result += chars.charAt(Math.floor(Math.random() * chars.length))
    }
    return result
  }

  /**
   * 生成随机数字
   */
  static randomNumber(min: number = 0, max: number = 100): number {
    return Math.floor(Math.random() * (max - min + 1)) + min
  }
}

// 使用示例
import { describe, it, expect } from '[@ohos](/user/ohos)/hypium'

export default function testUtilDemo() {
  describe('TestUtil示例', () => {
    it('should_create_mock_array', () => {
      const mockUsers = TestUtil.createMockArray(5, (i) => ({
        id: i,
        name: `User${i}`,
        email: `user${i}@example.com`
      }))

      expect(mockUsers.length).assertEqual(5)
      expect(mockUsers[0].name).assertEqual('User0')
    })

    it('should_wait_for_condition', async () => {
      let flag = false
      setTimeout(() => {
        flag = true
      }, 1000)

      await TestUtil.waitFor(() => flag)
      expect(flag).assertTrue()
    })

    it('should_generate_random_string', () => {
      const str1 = TestUtil.randomString(10)
      const str2 = TestUtil.randomString(10)

      expect(str1.length).assertEqual(10)
      expect(str2.length).assertEqual(10)
      expect(str1).assertNotEqual(str2)
    })
  })
}

关键要点

  1. 测试框架: 使用@ohos/hypium进行单元测试
  2. 测试结构: describe组织测试套件,it定义测试用例
  3. 断言: 使用expect进行结果验证
  4. 生命周期: beforeEach/afterEach管理测试环境
  5. Mock数据: 隔离外部依赖,使用Mock数据

最佳实践

  1. 命名规范: 测试用例名称清晰描述测试内容
  2. 独立性: 每个测试用例独立,互不影响
  3. 覆盖率: 关注边界条件和异常情况
  4. 可维护: 测试代码同样需要良好的结构
  5. 持续集成: 自动化运行测试,及时发现问题

更多关于HarmonyOS鸿蒙Next应用开发中如何编写单元测试?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,单元测试使用ArkTS编写,基于ArkUI Test框架。测试文件通常命名为xxx.test.ets,与源码同目录。使用@Test装饰器标记测试用例,@BeforeEach@AfterEach用于前置后置操作。断言方法如assert.equal()来自hilog@ohos.util。通过DevEco Studio的测试运行器执行。

在HarmonyOS Next应用开发中,编写单元测试主要依赖于ArkTS/ArkUI框架提供的测试能力。以下是核心方法:

  1. 单元测试编写

    • 使用@Test装饰器标记测试用例,通常与describeitbeforeEach等函数结合组织测试套件。
    • 通过ohos.application.testRunner模块的TestRunner API运行测试,支持异步测试和断言库(如expect)。
    • 示例:
      import { describe, it, expect } from '@ohos/hypium';
      
      describe('CalculatorTest', () => {
        it('add_test', () => {
          expect(1 + 1).assertEqual(2);
        });
      });
      
  2. UI自动化测试

    • 使用UiTest框架,通过Driver API模拟用户操作(如点击、滑动)。
    • 结合ComponentWindow对象定位UI元素,支持属性检查和事件触发。
    • 示例:
      import { Driver } from '@ohos.uitest';
      
      let driver = Driver.create();
      await driver.delayMs(1000);
      let button = await driver.findComponent(By.text('Submit'));
      await button.click();
      
  3. 代码质量保障

    • 集成静态检查工具(如ESLint)规范代码风格。
    • 利用ohos.previewerDevEco Studio的调试工具进行实时验证。
    • 结合持续集成(CI)自动执行测试,确保代码稳定性。

测试文件需放在模块的ohosTest目录下,并通过DevEco Studio的测试面板或命令行执行。

回到顶部