HarmonyOS开发:测试夹具Fixture与Setup

举报
Jack20 发表于 2026/06/24 15:12:47 2026/06/24
【摘要】 HarmonyOS开发:测试夹具Fixture与Setup📌 核心要点:测试夹具是保证测试隔离性和可重复性的关键机制——beforeAll/afterAll管套件级资源,beforeEach/afterEach管用例级状态,搞清楚谁在什么时候执行,才能写出不互相干扰的干净测试。 一、背景与动机你有没有遇到过这种诡异的情况:单独跑某个测试用例,稳稳通过;但跑整个测试套件,它就挂了?或者更离...

HarmonyOS开发:测试夹具Fixture与Setup

📌 核心要点:测试夹具是保证测试隔离性和可重复性的关键机制——beforeAll/afterAll管套件级资源,beforeEach/afterEach管用例级状态,搞清楚谁在什么时候执行,才能写出不互相干扰的干净测试。


一、背景与动机

你有没有遇到过这种诡异的情况:单独跑某个测试用例,稳稳通过;但跑整个测试套件,它就挂了?或者更离谱——昨天还能过,今天突然红了,代码一行没改?

大概率是测试用例之间"串了"。A用例修改了全局变量,B用例依赖这个变量的初始值,A先跑B就挂,A后跑B就过。这种时序依赖的Bug,排查起来能让人怀疑人生。

问题出在哪?没有做好测试数据的准备与清理。每个测试用例都应该在一个"干净"的环境中运行,就像每道菜都应该用洗干净的锅来做——你不能指望上一道菜的残渣对这道菜没影响。

Hypium提供了四个生命周期钩子来解决这个问题:beforeAllafterAllbeforeEachafterEach。它们就是你的"洗锅"工具。但光知道有这四个钩子不够,你得清楚哪个在什么时候执行、适合做什么、怎么搭配才不会踩坑。


二、核心原理

2.1 生命周期钩子执行时序

flowchart TB
    A[测试套件开始] --> B[beforeAll]
    B --> C{遍历测试用例}
    C --> D[beforeEach]
    D --> E[执行测试用例1]
    E --> F[afterEach]
    F --> G{还有用例?}
    G -->|| D
    G -->|| H[afterAll]
    H --> I[测试套件结束]

    classDef startStyle fill:#4CAF50,stroke:#388E3C,color:#fff,font-weight:bold
    classDef beforeAllStyle fill:#2196F3,stroke:#1976D2,color:#fff
    classDef eachStyle fill:#FF9800,stroke:#F57C00,color:#fff
    classDef testStyle fill:#9C27B0,stroke:#7B1FA2,color:#fff
    classDef afterAllStyle fill:#F44336,stroke:#D32F2F,color:#fff
    classDef decisionStyle fill:#607D8B,stroke:#455A64,color:#fff

    class A,I startStyle
    class B,H afterAllStyle
    class D,F eachStyle
    class E testStyle
    class C,G decisionStyle

关键规则:

  • beforeAll/afterAll:整个describe块只执行一次
  • beforeEach/afterEach:每个it用例执行前后各跑一次
  • afterEach即使测试失败也会执行(清理不能省)
  • 嵌套describe时,外层的钩子在内层用例前也会执行

2.2 四个钩子的职责划分

钩子 执行时机 适合做什么 不适合做什么
beforeAll 套件开始前 初始化共享资源、建立数据库连接、加载配置 创建每个用例需要独立使用的对象
afterAll 套件结束后 释放共享资源、关闭连接、清理临时文件 清理每个用例产生的中间状态
beforeEach 每个用例前 重置对象状态、准备测试数据、初始化变量 做只需要一次的耗时操作
afterEach 每个用例后 清理用例产生的临时数据、恢复环境 释放整个套件共享的资源

2.3 Fixture模式

Fixture(测试夹具)的核心思想是:测试的运行环境应该是可预测、可重复、可隔离的。它不是某个具体的API,而是一种设计模式:

Fixture = 测试前准备(Setup) + 测试后清理(Teardown)

在鸿蒙测试中,Fixture模式体现在三个层面:

  1. 数据Fixture:每个用例运行前准备好输入数据,运行后清除产生的数据
  2. 状态Fixture:重置全局状态(如单例、缓存),确保用例之间不串扰
  3. 资源Fixture:管理文件句柄、网络连接等外部资源的生命周期

三、代码实战

3.1 基础用法:四个生命周期钩子

// ShoppingCart.ets - 购物车业务代码
export class ShoppingCart {
  private items: Map<string, { name: string; price: number; quantity: number }> = new Map()

  addItem(id: string, name: string, price: number, quantity: number = 1): void {
    if (this.items.has(id)) {
      const existing = this.items.get(id)!
      existing.quantity += quantity
    } else {
      this.items.set(id, { name, price, quantity })
    }
  }

  removeItem(id: string): void {
    this.items.delete(id)
  }

  getTotal(): number {
    let total = 0
    this.items.forEach(item => {
      total += item.price * item.quantity
    })
    return total
  }

  getItemCount(): number {
    let count = 0
    this.items.forEach(item => {
      count += item.quantity
    })
    return count
  }

  clear(): void {
    this.items.clear()
  }

  isEmpty(): boolean {
    return this.items.size === 0
  }
}
// ShoppingCartTest.ets - 基础生命周期钩子
import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from '@ohos/hypium'
import { ShoppingCart } from '../../../main/ets/model/ShoppingCart'

export default function shoppingCartTest() {
  describe('ShoppingCart购物车', () => {
    let cart: ShoppingCart
    let testLog: string[] = []  // 记录执行顺序,仅用于演示

    beforeAll(() => {
      // 套件级初始化:只执行一次
      console.info('=== 购物车测试套件开始 ===')
      testLog.push('beforeAll')
    })

    afterAll(() => {
      // 套件级清理:只执行一次
      console.info('=== 购物车测试套件结束 ===')
      console.info(`执行顺序: ${testLog.join(' → ')}`)
      testLog = []
    })

    beforeEach(() => {
      // 用例级初始化:每个用例前都执行
      cart = new ShoppingCart()
      testLog.push('beforeEach')
    })

    afterEach(() => {
      // 用例级清理:每个用例后都执行
      cart.clear()
      cart = null
      testLog.push('afterEach')
    })

    it('addItem_添加商品_总价正确', 0, () => {
      testLog.push('test1')
      cart.addItem('p1', '手机', 3999)
      cart.addItem('p2', '手机壳', 49)
      assertEqual(cart.getTotal(), 4048)
    })

    it('addItem_重复添加_数量累加', 0, () => {
      testLog.push('test2')
      cart.addItem('p1', '手机', 3999, 1)
      cart.addItem('p1', '手机', 3999, 2)
      assertEqual(cart.getItemCount(), 3)
      assertEqual(cart.getTotal(), 3999 * 3)
    })

    it('removeItem_移除商品_总价更新', 0, () => {
      testLog.push('test3')
      cart.addItem('p1', '手机', 3999)
      cart.addItem('p2', '手机壳', 49)
      cart.removeItem('p1')
      assertEqual(cart.getTotal(), 49)
    })

    it('clear_清空购物车_为空', 0, () => {
      testLog.push('test4')
      cart.addItem('p1', '手机', 3999)
      cart.clear()
      assertTrue(cart.isEmpty())
      assertEqual(cart.getTotal(), 0)
    })
  })
}
// 执行顺序: beforeAll → beforeEach → test1 → afterEach
//         → beforeEach → test2 → afterEach
//         → beforeEach → test3 → afterEach
//         → beforeEach → test4 → afterEach → afterAll

3.2 进阶用法:嵌套describe与共享Fixture

// UserManager.ets - 用户管理业务代码
export interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
  createdAt: number
}

export class UserManager {
  private users: Map<string, User> = new Map()
  private idCounter: number = 0

  createUser(name: string, email: string, role: 'admin' | 'user' | 'guest' = 'user'): User {
    const id = `user_${++this.idCounter}`
    const user: User = {
      id,
      name,
      email,
      role,
      createdAt: Date.now()
    }
    this.users.set(id, user)
    return user
  }

  getUser(id: string): User | undefined {
    return this.users.get(id)
  }

  updateUser(id: string, updates: Partial<Pick<User, 'name' | 'email' | 'role'>>): User | null {
    const user = this.users.get(id)
    if (!user) return null
    Object.assign(user, updates)
    return user
  }

  deleteUser(id: string): boolean {
    return this.users.delete(id)
  }

  getUsersByRole(role: User['role']): User[] {
    const result: User[] = []
    this.users.forEach(user => {
      if (user.role === role) result.push(user)
    })
    return result
  }

  getUserCount(): number {
    return this.users.size
  }

  clear(): void {
    this.users.clear()
    this.idCounter = 0
  }
}
// UserManagerTest.ets - 嵌套describe中的Fixture
import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from '@ohos/hypium'
import { UserManager, User } from '../../../main/ets/model/UserManager'

export default function userManagerTest() {
  describe('UserManager用户管理', () => {
    let manager: UserManager

    beforeAll(() => {
      // 套件级:初始化共享配置
      console.info('UserManager测试开始')
    })

    afterAll(() => {
      // 套件级:释放资源
      console.info('UserManager测试结束')
    })

    beforeEach(() => {
      // 每个用例前创建全新的manager,保证隔离
      manager = new UserManager()
    })

    afterEach(() => {
      // 每个用例后清理
      manager.clear()
      manager = null
    })

    describe('createUser创建用户', () => {
      it('createUser_正常创建_返回用户对象', 0, () => {
        const user = manager.createUser('张三', 'zhang@test.com')
        assertEqual(user.name, '张三')
        assertEqual(user.email, 'zhang@test.com')
        assertEqual(user.role, 'user')  // 默认角色
        assertNotNull(user.id)
      })

      it('createUser_指定角色_角色正确', 0, () => {
        const user = manager.createUser('管理员', 'admin@test.com', 'admin')
        assertEqual(user.role, 'admin')
      })

      it('createUser_连续创建_ID递增', 0, () => {
        const user1 = manager.createUser('用户1', 'u1@test.com')
        const user2 = manager.createUser('用户2', 'u2@test.com')
        assertTrue(user2.id > user1.id)
      })
    })

    describe('updateUser更新用户', () => {
      let testUser: User

      beforeEach(() => {
        // 内层beforeEach:为updateUser的用例准备基础数据
        testUser = manager.createUser('原始名', 'original@test.com', 'user')
      })

      it('updateUser_更新名字_名字变更', 0, () => {
        const updated = manager.updateUser(testUser.id, { name: '新名字' })
        assertEqual(updated!.name, '新名字')
        assertEqual(updated!.email, 'original@test.com')  // 其他字段不变
      })

      it('updateUser_更新角色_角色变更', 0, () => {
        const updated = manager.updateUser(testUser.id, { role: 'admin' })
        assertEqual(updated!.role, 'admin')
      })

      it('updateUser_不存在的ID_返回null', 0, () => {
        const result = manager.updateUser('nonexistent', { name: '测试' })
        assertEqual(result, null)
      })
    })

    describe('getUsersByRole按角色查询', () => {
      beforeAll(() => {
        // 内层beforeAll:准备一批测试用户
        // 注意:这里不能用外层的manager,因为外层beforeEach会重建
        // 所以这里只做日志记录,实际数据在每个用例中准备
        console.info('getUsersByRole测试组开始')
      })

      it('getUsersByRole_混合角色_返回指定角色', 0, () => {
        manager.createUser('管理员A', 'a@test.com', 'admin')
        manager.createUser('用户B', 'b@test.com', 'user')
        manager.createUser('管理员C', 'c@test.com', 'admin')
        manager.createUser('访客D', 'd@test.com', 'guest')

        const admins = manager.getUsersByRole('admin')
        assertEqual(admins.length, 2)
      })

      it('getUsersByRole_无匹配角色_返回空数组', 0, () => {
        manager.createUser('用户A', 'a@test.com', 'user')
        const admins = manager.getUsersByRole('admin')
        assertEqual(admins.length, 0)
      })
    })
  })
}

3.3 完整示例:Fixture工厂模式

当项目规模变大,测试数据的准备逻辑会越来越复杂。如果每个测试文件都自己写beforeEach准备数据,重复代码会爆炸。这时候需要引入Fixture工厂模式——把数据准备和清理逻辑封装成可复用的模块。

// TestFixture.ets - 测试夹具工厂
export interface FixtureContext {
  [key: string]: unknown
}

export type SetupFn = (context: FixtureContext) => void | Promise<void>
export type TeardownFn = (context: FixtureContext) => void | Promise<void>

export interface FixtureConfig {
  name: string
  setup: SetupFn
  teardown: TeardownFn
}

// 通用测试夹具工厂
export class TestFixtureFactory {
  private fixtures: FixtureConfig[] = []
  private context: FixtureContext = {}

  // 注册夹具
  register(fixture: FixtureConfig): TestFixtureFactory {
    this.fixtures.push(fixture)
    return this  // 支持链式调用
  }

  // 执行所有setup
  async setupAll(): Promise<FixtureContext> {
    for (const fixture of this.fixtures) {
      await fixture.setup(this.context)
    }
    return this.context
  }

  // 执行所有teardown(逆序执行,类似栈的LIFO)
  async teardownAll(): Promise<void> {
    for (let i = this.fixtures.length - 1; i >= 0; i--) {
      await this.fixtures[i].teardown(this.context)
    }
    this.context = {}
  }

  // 重置上下文(不执行teardown)
  reset(): void {
    this.context = {}
  }

  getContext(): FixtureContext {
    return this.context
  }
}

// 预置夹具:用户数据
export function createUserFixture(): FixtureConfig {
  return {
    name: 'user-fixture',
    setup: (ctx) => {
      ctx['testUser'] = {
        id: 'test_user_001',
        name: '测试用户',
        email: 'test@example.com',
        role: 'user'
      }
      ctx['adminUser'] = {
        id: 'test_admin_001',
        name: '测试管理员',
        email: 'admin@example.com',
        role: 'admin'
      }
    },
    teardown: (ctx) => {
      delete ctx['testUser']
      delete ctx['adminUser']
    }
  }
}

// 预置夹具:配置数据
export function createConfigFixture(): FixtureConfig {
  return {
    name: 'config-fixture',
    setup: (ctx) => {
      ctx['config'] = {
        apiTimeout: 5000,
        maxRetryCount: 3,
        debugMode: true
      }
    },
    teardown: (ctx) => {
      delete ctx['config']
    }
  }
}

// 预置夹具:临时数据存储
export function createTempStoreFixture(): FixtureConfig {
  const store = new Map<string, unknown>()
  return {
    name: 'temp-store-fixture',
    setup: (ctx) => {
      store.clear()
      ctx['tempStore'] = store
    },
    teardown: (ctx) => {
      store.clear()
      delete ctx['tempStore']
    }
  }
}
// TestFixtureDemo.ets - 使用Fixture工厂的完整测试
import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from '@ohos/hypium'
import {
  TestFixtureFactory,
  createUserFixture,
  createConfigFixture,
  createTempStoreFixture
} from '../../../main/ets/test/TestFixture'

export default function fixtureDemoTest() {
  describe('Fixture工厂模式演示', () => {
    let factory: TestFixtureFactory
    let context: FixtureContext

    beforeAll(async () => {
      // 套件级:注册并初始化所有夹具
      factory = new TestFixtureFactory()
      factory
        .register(createUserFixture())
        .register(createConfigFixture())
        .register(createTempStoreFixture())

      context = await factory.setupAll()
      console.info('所有夹具初始化完成')
    })

    afterAll(async () => {
      // 套件级:逆序清理所有夹具
      await factory.teardownAll()
      console.info('所有夹具清理完成')
    })

    it('userFixture_测试用户已就绪', 0, () => {
      const testUser = context['testUser'] as Record<string, string>
      assertEqual(testUser.name, '测试用户')
      assertEqual(testUser.role, 'user')
    })

    it('userFixture_管理员已就绪', 0, () => {
      const adminUser = context['adminUser'] as Record<string, string>
      assertEqual(adminUser.role, 'admin')
    })

    it('configFixture_配置已加载', 0, () => {
      const config = context['config'] as Record<string, unknown>
      assertEqual(config.apiTimeout, 5000)
      assertEqual(config.debugMode, true)
    })

    it('tempStoreFixture_临时存储可用', 0, () => {
      const store = context['tempStore'] as Map<string, unknown>
      store.set('test_key', 'test_value')
      assertEqual(store.get('test_key'), 'test_value')
    })

    it('tempStoreFixture_用例间互不干扰', 0, () => {
      // 上一个用例虽然写了数据,但tempStore是共享的
      // 如果需要隔离,应在beforeEach中清空
      const store = context['tempStore'] as Map<string, unknown>
      // 验证store存在且可用
      assertNotNull(store)
      assertEqual(store.size >= 0, true)
    })
  })

  describe('独立Fixture测试', () => {
    let factory: TestFixtureFactory

    beforeEach(async () => {
      // 用例级:每个用例都有独立的夹具上下文
      factory = new TestFixtureFactory()
      factory.register(createUserFixture())
      await factory.setupAll()
    })

    afterEach(async () => {
      await factory.teardownAll()
    })

    it('独立上下文_互不影响', 0, () => {
      const ctx = factory.getContext()
      const testUser = ctx['testUser'] as Record<string, string>
      assertEqual(testUser.name, '测试用户')
    })

    it('独立上下文_每次都是全新数据', 0, () => {
      const ctx = factory.getContext()
      const testUser = ctx['testUser'] as Record<string, string>
      // 修改不会影响其他用例
      testUser.name = '被修改的名字'
      assertEqual(testUser.name, '被修改的名字')
    })

    it('独立上下文_上一个用例的修改不可见', 0, () => {
      const ctx = factory.getContext()
      const testUser = ctx['testUser'] as Record<string, string>
      // 因为beforeEach重新setup了,所以数据是初始值
      assertEqual(testUser.name, '测试用户')
    })
  })
}

四、踩坑与注意事项

坑点1:beforeEach不是万能的隔离方案

beforeEach确实能保证每个用例前都重新初始化对象,但如果你在beforeEach里做了异步操作(如读文件、网络请求),用例可能在数据还没准备好时就开始执行了。异步的beforeEach必须用async/await,否则用例拿到的可能是undefined

坑点2:afterAll里别访问用例级别的变量

afterAll执行时,beforeEach里创建的变量已经被afterEach清掉了。如果你在afterAll里试图访问cartmanager这些用例级变量,大概率拿到的是nullafterAll只适合清理beforeAll里创建的资源。

坑点3:嵌套describe的钩子执行顺序

外层beforeEach在内层beforeEach之前执行,外层afterEach在内层afterEach之后执行。执行顺序是:外层beforeEach → 内层beforeEach → 测试用例 → 内层afterEach → 外层afterEach。如果你在内外层都初始化了同名变量,内层会覆盖外层——这可能是你想要的,也可能不是。

坑点4:afterEach里别抛异常

afterEach的主要职责是清理,如果清理逻辑本身抛了异常,不仅当前用例的失败信息会被覆盖,后续用例的环境可能也没清理干净。在afterEach里做好try-catch,确保清理逻辑不会因为异常而中断

坑点5:共享状态是测试不稳定的头号杀手

多个用例共享同一个对象,A用例修改了状态,B用例就受影响。解决方案很简单:每个用例都创建自己的实例,用beforeEach来做这件事。如果创建成本太高,用深拷贝代替引用共享。

坑点6:别在beforeAll里做耗时操作后不检查

beforeAll里初始化数据库连接、加载配置文件,这些操作可能失败。如果不检查返回值,用例跑起来拿到一个null连接,报的错会让你以为是业务代码的问题,实际是初始化就失败了。在beforeAll里加上断言或至少加上null检查

坑点7:测试数据别用生产数据

beforeAll里从生产数据库拉数据做测试?别闹了。生产数据随时会变,你的测试就随时可能挂。测试数据必须是可控的,要么硬编码,要么用工厂方法生成,要么用专门的测试数据库。


五、HarmonyOS 6适配说明

API差异表

功能/接口 HarmonyOS 5 HarmonyOS 6 变更说明
生命周期钩子 beforeAll/afterAll等 同左 + async原生支持 异步钩子更稳定
Fixture工厂 需自实现 @Fixture装饰器 内置夹具注册机制
测试上下文 手动管理 TestContext对象 框架级上下文管理
并行测试隔离 @Isolated装饰器 用例级隔离支持
资源自动清理 手动afterAll AutoCleanup注解 自动释放注册资源

行为变更

  1. @Fixture装饰器:HarmonyOS 6引入了@Fixture装饰器,可以直接在describe上标注夹具配置,不需要手动在beforeAll/afterAll中注册。

  2. TestContext对象:新增TestContext对象,在钩子函数中通过参数传入,替代手动管理的共享变量,避免命名冲突和类型不安全。

  3. @Isolated并行隔离:标记@Isolated的测试用例会在独立的上下文中执行,即使并行运行也不会互相干扰。

适配代码

// HarmonyOS 6 Fixture装饰器
import { describe, it, Fixture, TestContext, Isolated } from '@ohos/hypium'

@Fixture({
  setup: (ctx: TestContext) => {
    ctx.set('testUser', { id: '001', name: '测试用户' })
  },
  teardown: (ctx: TestContext) => {
    ctx.clear()
  }
})
describe('HarmonyOS 6夹具示例', () => {
  it('fixture_自动注入上下文', 0, (ctx: TestContext) => {
    const user = ctx.get('testUser') as Record<string, string>
    assertEqual(user.name, '测试用户')
  })

  @Isolated
  it('isolated_独立执行环境', 0, () => {
    // 这个用例在独立上下文中运行,不受其他用例影响
    assertEqual(1 + 1, 2)
  })
})

六、总结

维度 评价
学习难度 ⭐⭐
使用频率 ⭐⭐⭐⭐⭐
重要程度 ⭐⭐⭐⭐⭐

测试夹具的核心就一句话:每个用例都在干净的环境中运行,跑完不留痕迹beforeAll管套件级共享资源的初始化和释放,beforeEach管用例级状态的准备和清理。别偷懒跳过清理步骤,也别在钩子里做超出职责的事。当你发现测试结果不稳定、时过时不过的时候,第一反应应该是检查Fixture有没有做好隔离——十次有九次都是这个原因。好的Fixture让测试稳定如磐石,差的Fixture让测试脆弱如蛛丝

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。