如何对包含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
相关产品推荐
相关产品推荐

