如何为iOS中依赖系统音量的方法编写单元测试?
解决Swift中AVAudioSession outputVolume只读属性的单元测试问题
核心思路:通过依赖注入抽象AVAudioSession
直接依赖AVAudioSession.sharedInstance()会导致测试与系统实现强耦合,最可靠的解决方式是将音频会话抽象为协议,让业务代码依赖协议而非具体实现,测试时通过Mock实例自由控制outputVolume的值。
步骤1:定义音频会话协议
创建包含outputVolume属性的协议,让系统AVAudioSession自动遵守该协议:
protocol AudioSessionProtocol { var outputVolume: Float { get } } extension AVAudioSession: AudioSessionProtocol {}
步骤2:重构原代码,注入依赖
修改包含trigger()函数的类,将音频会话作为初始化参数传入(默认使用系统单例,不影响原有业务逻辑):
class VolumeHandler { static let THRESHOLD: Float = 0.5 private let audioSession: AudioSessionProtocol // 默认使用系统AVAudioSession单例,兼容原有调用方式 init(audioSession: AudioSessionProtocol = AVAudioSession.sharedInstance()) { self.audioSession = audioSession } func trigger() { let currentVolume = audioSession.outputVolume if currentVolume < Self.THRESHOLD { do_something { success in // 业务回调逻辑 } } else { fire_method() } } // 带回调的API请求方法,用于测试 func do_something(completion: @escaping (Bool) -> Void) { // 实际API请求逻辑 URLSession.shared.dataTask(with: URL(string: "https://example.com/api")!) { _, _, error in completion(error == nil) }.resume() } func fire_method() { // 其他业务逻辑 } }
步骤3:编写Mock音频会话类
创建Mock类模拟不同音量值,覆盖所有分支测试场景:
class MockAudioSession: AudioSessionProtocol { var outputVolume: Float init(outputVolume: Float) { self.outputVolume = outputVolume } }
步骤4:编写单元测试
使用XCTest框架测试不同音量分支,以及do_something的API回调逻辑:
import XCTest @testable import YourAppName class VolumeHandlerTests: XCTestCase { var volumeHandler: VolumeHandler! override func setUp() { super.setUp() volumeHandler = VolumeHandler() } override func tearDown() { volumeHandler = nil super.tearDown() } // 测试音量低于阈值时触发do_something及API回调 func testTrigger_VolumeBelowThreshold_CallsDoSomething() { let mockSession = MockAudioSession(outputVolume: 0.3) volumeHandler = VolumeHandler(audioSession: mockSession) let apiExpectation = expectation(description: "API请求回调完成") var callbackSuccess = false volumeHandler.do_something { success in callbackSuccess = success apiExpectation.fulfill() } waitForExpectations(timeout: 5, handler: nil) XCTAssertTrue(callbackSuccess) } // 测试音量高于阈值时触发fire_method func testTrigger_VolumeAboveThreshold_CallsFireMethod() { let mockSession = MockAudioSession(outputVolume: 0.7) // 用Spy类验证方法调用 class SpyVolumeHandler: VolumeHandler { var fireMethodCalled = false override func fire_method() { fireMethodCalled = true } } let spyHandler = SpyVolumeHandler(audioSession: mockSession) spyHandler.trigger() XCTAssertTrue(spyHandler.fireMethodCalled) } }
关于真机运行单元测试的补充
Xcode原生支持在真机上运行单元测试:
- 连接真机后,在Xcode顶部设备选择器中选中该设备
- 确保真机已添加到开发者账号的设备列表
- 直接点击测试按钮即可运行,操作逻辑与模拟器完全一致
不过通过上述依赖注入方案,所有核心逻辑都能在模拟器中完成测试,无需依赖真机环境。
内容的提问来源于stack exchange,提问作者Shilpi Roy
相关产品推荐
相关产品推荐

