iOS 17:如何在单元测试中使用HKWorkoutBuilder创建HKWorkout?
iOS 17里HKWorkout的传统初始化方法已被弃用,官方推荐用HKWorkoutBuilder,但单元测试场景下,HKWorkoutBuilder依赖HKHealthStore,而真实的HKHealthStore需要用户授权才能生成HKWorkoutActivity实例,单元测试没法处理授权弹窗,导致正常使用HKWorkoutBuilder受阻。下面是几个可行的解决思路:
方案1:Mock HKHealthStore 绕过授权检查
单元测试的核心是隔离依赖,我们可以Mock一个HKHealthStore的替代类,模拟授权状态为已授权,让HKWorkoutBuilder能正常工作。
步骤如下:
- 先定义一个协议,包含
HKWorkoutBuilder需要用到的HKHealthStore方法:
protocol HealthStoreProtocol { func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus // 按需补充其他需要的方法 } // 让真实HKHealthStore遵守这个协议 extension HKHealthStore: HealthStoreProtocol {}
- 创建Mock类,固定返回已授权状态:
class MockHealthStore: HealthStoreProtocol { func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus { return .sharingAuthorized } }
- 在测试中用Mock类初始化
HKWorkoutBuilder并构建HKWorkout:
func testCreateHKWorkout() { let mockHealthStore = MockHealthStore() let workoutConfig = HKWorkoutConfiguration() workoutConfig.activityType = .running workoutConfig.locationType = .outdoor guard let builder = HKWorkoutBuilder(healthStore: mockHealthStore, configuration: workoutConfig, device: nil) else { XCTFail("创建HKWorkoutBuilder失败") return } let startDate = Date() let endDate = startDate.addingTimeInterval(300) builder.beginCollection(withStart: startDate) { success, _ in XCTAssertTrue(success) builder.endCollection(withEnd: endDate) { success, _ in XCTAssertTrue(success) builder.finishWorkout { workout, _ in XCTAssertNotNil(workout) // 这里拿到的workout即可用于测试相关服务 } } } }
如果HKWorkoutBuilder内部还有其他依赖HKHealthStore的逻辑,记得在Mock类里补充对应的模拟实现。
方案2:调用私有初始化方法(仅限单元测试)
虽然iOS 17弃用了公开的HKWorkout初始化方法,但私有初始化方法仍存在,单元测试环境下可以通过Runtime调用它直接创建实例,完全不用依赖HKWorkoutBuilder和授权。
示例代码:
func testCreateHKWorkoutWithPrivateInit() { let activityType = HKWorkoutActivityType.running let startDate = Date() let endDate = startDate.addingTimeInterval(300) guard let initSelector = NSSelectorFromString("initWithActivityType:startDate:endDate:") else { XCTFail("找不到私有初始化方法") return } if HKWorkout.responds(to: initSelector) { let workout = HKWorkout.perform(initSelector, with: activityType, with: startDate, with: endDate)?.takeUnretainedValue() as? HKWorkout XCTAssertNotNil(workout) } else { XCTFail("私有初始化方法不可用") } }
⚠️ 注意:这种方法依赖私有API,绝对不能在生产代码中使用,仅适用于单元测试场景——苹果随时可能在后续系统版本中移除该私有方法。
方案3:协议封装HKWorkout,用测试替身替代
如果你的业务代码并不依赖HKWorkout的全部功能,只是需要访问它的部分属性(比如activityType、duration等),可以定义一个协议封装这些属性,测试时用自定义的测试替身类代替真实的HKWorkout。
步骤如下:
- 定义协议:
protocol WorkoutProtocol { var activityType: HKWorkoutActivityType { get } var startDate: Date { get } var endDate: Date { get } var duration: TimeInterval { get } // 按需添加业务需要的其他属性 } // 让真实HKWorkout遵守协议 extension HKWorkout: WorkoutProtocol {}
- 创建测试替身类:
class MockWorkout: WorkoutProtocol { var activityType: HKWorkoutActivityType var startDate: Date var endDate: Date var duration: TimeInterval init(activityType: HKWorkoutActivityType, startDate: Date, endDate: Date) { self.activityType = activityType self.startDate = startDate self.endDate = endDate self.duration = endDate.timeIntervalSince(startDate) } }
- 业务代码依赖
WorkoutProtocol,测试时注入MockWorkout:
// 业务代码示例 class WorkoutService { func processWorkout(_ workout: WorkoutProtocol) -> String { return "运动类型:\(workout.activityType.rawValue),时长:\(workout.duration)秒" } } // 单元测试示例 func testProcessWorkout() { let mockWorkout = MockWorkout(activityType: .running, startDate: Date(), endDate: Date().addingTimeInterval(300)) let service = WorkoutService() let result = service.processWorkout(mockWorkout) XCTAssertEqual(result, "运动类型:\(HKWorkoutActivityType.running.rawValue),时长:300.0秒") }
这种方法完全隔离了对HealthKit框架的依赖,测试更稳定,不受系统版本变更影响。
内容的提问来源于stack exchange,提问作者Vladimir Amiorkov

