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

如何对包含DispatchQueue.main.async的异步Swift代码进行单元测试?

我懂你这种头疼的情况——本地跑Touch ID认证完全正常,一到单元测试就出问题,这简直是生物识别代码测试的标配坑!核心原因很简单:单元测试环境没法触发真实的系统生物识别弹窗,也没法控制它的返回结果。下面给你几个实用的解决方案,亲测有效:

解决方案1:依赖注入替换LAContext

你现在的代码应该是直接用了一个类内部的authenticationContext(大概率是LAContext的实例),这种硬编码的依赖在测试里根本没法控制。我们可以把LAContext作为参数传入方法,给个默认值不影响原有调用,这样测试时就能替换成我们的模拟对象了。

先修改你的认证方法:

import LocalAuthentication

// 给方法添加LAContext参数,默认用LAContext(),不破坏原有调用逻辑
public func authenticateTouchID(using context: LAContext = LAContext(), completion: @escaping (_ result: Bool, _ error: Error?) -> Void) {
    context.evaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        localizedReason: NSLocalizedString("temp", comment: ""),
        reply: { (result, error) -> Void in
            DispatchQueue.main.async {
                guard let error = error else {
                    completion(result, nil)
                    return
                }
                completion(result, error)
            }
        }
    )
}

然后写一个MockLAContext类,继承自LAContext,重写evaluatePolicy方法来模拟各种场景:

class MockLAContext: LAContext {
    // 预设我们想要的返回结果和错误
    var mockSuccessResult: Bool = true
    var mockError: Error? = nil
    
    override func evaluatePolicy(_ policy: LAPolicy, localizedReason: String, reply: @escaping (Bool, Error?) -> Void) {
        // 直接返回预设值,不用调用真实的生物识别
        reply(mockSuccessResult, mockError)
    }
}

接下来写单元测试,用这个Mock类来模拟成功和失败的情况:

import XCTest
import LocalAuthentication

class TouchIDTests: XCTestCase {
    func testAuthenticateTouchID_Success() {
        let mockContext = MockLAContext()
        mockContext.mockSuccessResult = true
        mockContext.mockError = nil
        
        // 用XCTestExpectation等待异步回调
        let completionExpectation = expectation(description: "Authentication completes successfully")
        
        authenticateTouchID(using: mockContext) { result, error in
            XCTAssertTrue(result, "Authentication should succeed")
            XCTAssertNil(error, "No error should be returned")
            completionExpectation.fulfill()
        }
        
        waitForExpectations(timeout: 1.0, handler: nil)
    }
    
    func testAuthenticateTouchID_UserCancellation() {
        let mockContext = MockLAContext()
        mockContext.mockSuccessResult = false
        mockContext.mockError = LAError(.userCancel)
        
        let completionExpectation = expectation(description: "Authentication fails due to user cancel")
        
        authenticateTouchID(using: mockContext) { result, error in
            XCTAssertFalse(result, "Authentication should fail")
            XCTAssertEqual(error as? LAError, LAError(.userCancel), "Error should be user cancellation")
            completionExpectation.fulfill()
        }
        
        waitForExpectations(timeout: 1.0, handler: nil)
    }
}
解决方案2:用协议抽象生物识别逻辑(更灵活)

如果觉得继承LAContext不够灵活,可以抽象一个协议,让LAContext遵守它,这样测试时的Mock类只需要实现协议就行,不用继承系统类:

import LocalAuthentication

// 定义协议,只包含我们需要的方法
protocol BiometricAuthentication {
    func evaluatePolicy(_ policy: LAPolicy, localizedReason: String, reply: @escaping (Bool, Error?) -> Void)
}

// 让系统的LAContext自动遵守这个协议
extension LAContext: BiometricAuthentication {}

// 修改认证方法,依赖这个协议
public func authenticateTouchID(using authenticator: BiometricAuthentication = LAContext(), completion: @escaping (_ result: Bool, _ error: Error?) -> Void) {
    authenticator.evaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        localizedReason: NSLocalizedString("temp", comment: ""),
        reply: { (result, error) -> Void in
            DispatchQueue.main.async {
                guard let error = error else {
                    completion(result, nil)
                    return
                }
                completion(result, error)
            }
        }
    )
}

然后写Mock类:

class MockBiometricAuthenticator: BiometricAuthentication {
    // 可以记录方法是否被调用,方便验证逻辑
    var evaluatePolicyWasCalled = false
    var mockResult: Bool = true
    var mockError: Error? = nil
    
    func evaluatePolicy(_ policy: LAPolicy, localizedReason: String, reply: @escaping (Bool, Error?) -> Void) {
        evaluatePolicyWasCalled = true
        reply(mockResult, mockError)
    }
}

对应的单元测试还能验证方法是否被正确调用:

func testAuthenticateTouchID_CallsEvaluatePolicy() {
    let mockAuthenticator = MockBiometricAuthenticator()
    let expectation = expectation(description: "Authentication method is called")
    
    authenticateTouchID(using: mockAuthenticator) { _, _ in
        XCTAssertTrue(mockAuthenticator.evaluatePolicyWasCalled, "Evaluate policy should be invoked")
        expectation.fulfill()
    }
    
    waitForExpectations(timeout: 1.0, handler: nil)
}
几个额外的小提醒
  • 单元测试里一定要用XCTestExpectation来等待异步回调,不然测试会在回调执行前结束,导致断言失败
  • 不要在单元测试里尝试调用真实的生物识别,测试环境根本不支持,只会返回错误
  • 如果你的authenticationContext是类的属性,那也可以在测试前直接把这个属性替换成Mock对象,不用修改方法参数(适合不想改原有方法签名的情况)

内容的提问来源于stack exchange,提问作者shreekant

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 07:31:14