You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 16:02:26