如何在单元测试中触发NSFileCoordinator错误并验证代码逻辑?
解决NSFileCoordinator异步接口单元测试中模拟协调错误的问题
方法1:通过自定义NSFilePresenter触发准备阶段错误
NSFileCoordinator文档明确说明,关联的文件演示器准备写入失败时,会直接返回error且不执行写入回调。你可以手动实现一个抛出错误的文件演示器:
class FailingFilePresenter: NSObject, NSFilePresenter { var presentedItemURL: URL? var presentedItemOperationQueue: OperationQueue = .main func prepareForWriting(at url: URL) throws { // 抛出自定义错误,触发NSFileCoordinator返回错误 throw NSError(domain: NSCocoaErrorDomain, code: NSFileWriteUnknownError, userInfo: [NSLocalizedDescriptionKey: "模拟演示器准备失败"]) } }
在单元测试中关联该演示器到目标URL:
let testURL = // 你的测试文件URL let failingPresenter = FailingFilePresenter() failingPresenter.presentedItemURL = testURL NSFileCoordinator.addFilePresenter(failingPresenter) // 调用异步coordinateWriting方法,此时应触发协调错误 do { try await yourCoordinateWritingMethod(at: testURL) XCTFail("预期抛出协调错误,但未抛出") } catch { XCTAssertEqual((error as NSError).code, NSFileWriteUnknownError) } // 测试结束后移除演示器 NSFileCoordinator.removeFilePresenter(failingPresenter)
方法2:通过文件系统权限触发系统级协调错误
修改测试文件的权限,让NSFileCoordinator无法完成协调流程:
let testURL = // 你的测试文件URL let fileManager = FileManager.default // 设置文件为只读权限 try fileManager.setAttributes([.posixPermissions: 0o444], ofItemAtPath: testURL.path) // 调用异步方法,NSFileCoordinator会因无法获取写入权限返回错误 do { try await yourCoordinateWritingMethod(at: testURL) XCTFail("预期抛出权限错误,但未抛出") } catch { XCTAssertEqual((error as NSError).code, NSFileWriteNoPermissionError) } // 测试后恢复文件权限 try fileManager.setAttributes([.posixPermissions: 0o644], ofItemAtPath: testURL.path)
方法3:子类化NSFileCoordinator直接模拟错误
如果不需要真实文件系统交互,可以子类化NSFileCoordinator直接返回错误:
class MockFailingFileCoordinator: NSFileCoordinator { override func coordinateWritingItem(at url: URL, options: NSFileCoordinator.WritingOptions, error: NSErrorPointer, byAccessor accessor: @escaping (URL) -> Void) { // 直接赋值error,不执行accessor回调 error?.pointee = NSError(domain: NSCocoaErrorDomain, code: NSFileWriteInvalidFileNameError, userInfo: nil) } }
在你的异步接口中加入依赖注入,允许传入自定义NSFileCoordinator实例:
// 修改异步方法支持注入协调器 func coordinateWriting(at url: URL, coordinator: NSFileCoordinator = NSFileCoordinator()) async throws { try await withCheckedThrowingContinuation { continuation in var coordinatorError: NSError? coordinator.coordinateWritingItem(at: url, options: .forReplacing, error: &coordinatorError) { writingURL in // 写入逻辑 continuation.resume(returning: ()) } if let error = coordinatorError { continuation.resume(throwing: error) } } } // 单元测试中使用Mock类 let mockCoordinator = MockFailingFileCoordinator() do { try await coordinateWriting(at: testURL, coordinator: mockCoordinator) XCTFail("预期抛出模拟错误,但未抛出") } catch { XCTAssertEqual((error as NSError).code, NSFileWriteInvalidFileNameError) }
关于CheckedContinuation的安全处理
为避免重复resume,确保在withCheckedThrowingContinuation中只执行一次resume:
- 不要同时在coordinator的error分支和accessor回调中调用resume
- 可通过标志位跟踪是否已resume,确保逻辑原子性
优化后的异步方法示例:
func coordinateWriting(at url: URL) async throws { try await withCheckedThrowingContinuation { continuation in var didResume = false let resumeOnce: (Result<Void, Error>) -> Void = { result in guard !didResume else { return } didResume = true switch result { case .success: continuation.resume(returning: ()) case .failure(let error): continuation.resume(throwing: error) } } var coordinatorError: NSError? NSFileCoordinator().coordinateWritingItem(at: url, options: .forReplacing, error: &coordinatorError) { writingURL in // 执行写入逻辑,写入失败则调用resumeOnce(.failure(error)) resumeOnce(.success(())) } if let error = coordinatorError { resumeOnce(.failure(error)) } } }
内容的提问来源于stack exchange,提问作者Gene Bogdanovich
相关产品推荐
相关产品推荐

