如何为Xcode源码编辑器扩展测试XCSourceEditorCommandInvocation?
解决Xcode源码编辑器扩展测试中无法创建XCSourceEditorCommandInvocation的问题
XCSourceEditorCommandInvocation是XcodeKit提供的系统类,没有公开的初始化方法,无法直接实例化用于测试。下面是几种可行的解决方案,按推荐优先级排序:
1. 抽象依赖(推荐)
通过定义协议抽象Invocation的核心能力,让你的Command依赖协议而非具体的系统类,这样测试时可以轻松创建模拟实现。
步骤1:定义协议
import XcodeKit protocol CommandInvocationProtocol { var buffer: XCSourceTextBuffer { get } var selection: XCSourceTextRange { get } // 按需添加其他需要的属性/方法 } // 让系统类自动遵守协议 extension XCSourceEditorCommandInvocation: CommandInvocationProtocol {}
步骤2:修改Command类
调整perform方法,让核心逻辑依赖协议,同时保留系统要求的方法作为入口:
import XcodeKit class SourceEditorCommand: NSObject, XCSourceEditorCommand { // 核心逻辑方法,依赖协议 func perform(with invocation: CommandInvocationProtocol, completionHandler: @escaping (Error?) -> Void) { // 原来的业务逻辑,比如操作invocation.buffer let currentLine = invocation.buffer.lines[invocation.selection.start.line] as? String ?? "" invocation.buffer.lines[invocation.selection.start.line] = "Modified: \(currentLine)" completionHandler(nil) } // 系统要求的方法,转发到核心逻辑 func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void) { perform(with: invocation, completionHandler: completionHandler) } }
步骤3:编写测试
创建模拟Invocation,注入预设的buffer和selection:
import Testing import XcodeKit @testable import TestingToolsExtension class MockInvocation: CommandInvocationProtocol { var buffer: XCSourceTextBuffer var selection: XCSourceTextRange init(buffer: XCSourceTextBuffer, selection: XCSourceTextRange) { self.buffer = buffer self.selection = selection } } struct TestingToolsTests { @Test func commandModifiesLineCorrectly() async throws { // 准备测试数据 let testBuffer = XCSourceTextBuffer() testBuffer.lines = ["Original line"] let testSelection = XCSourceTextRange( start: XCSourceTextPosition(line: 0, column: 0), end: XCSourceTextPosition(line: 0, column: 0) ) let mockInvocation = MockInvocation(buffer: testBuffer, selection: testSelection) let sut = SourceEditorCommand() let completionExpectation = XCTestExpectation(description: "Command completes") // 执行测试 sut.perform(with: mockInvocation, completionHandler: { error in XCTAssertNil(error) // 验证结果 XCTAssertEqual(testBuffer.lines.first as? String, "Modified: Original line") completionExpectation.fulfill() }) await fulfillment(of: [completionExpectation], timeout: 1.0) } }
2. 使用OCMock创建模拟对象
如果不想修改现有代码结构,可以用OCMock库直接模拟XCSourceEditorCommandInvocation实例:
import Testing import XcodeKit import OCMock @testable import TestingToolsExtension struct TestingToolsTests { @Test func exampleTest() async throws { // 创建模拟Invocation let mockInvocation = OCMClassMock(XCSourceEditorCommandInvocation.self) // 预设buffer和selection let testBuffer = XCSourceTextBuffer() testBuffer.lines = ["Test line"] OCMStub(mockInvocation.buffer).andReturn(testBuffer) let testSelection = XCSourceTextRange( start: XCSourceTextPosition(line: 0, column: 0), end: XCSourceTextPosition(line: 0, column: 0) ) OCMStub(mockInvocation.selection).andReturn(testSelection) let sut = SourceEditorCommand() let completionExpectation = XCTestExpectation(description: "Command completes") sut.perform(with: mockInvocation as! XCSourceEditorCommandInvocation, completionHandler: { error in XCTAssertNil(error) XCTAssertEqual(testBuffer.lines.first as? String, "Expected modified line") completionExpectation.fulfill() }) await fulfillment(of: [completionExpectation], timeout: 1.0) } }
3. 私有API反射(不推荐)
通过Objective-C运行时动态创建实例并设置属性,这种方法依赖Xcode的私有实现,苹果随时可能修改,稳定性差:
import XcodeKit import ObjectiveC private var bufferAssocKey = "bufferAssocKey" private var selectionAssocKey = "selectionAssocKey" func createMockInvocation(buffer: XCSourceTextBuffer, selection: XCSourceTextRange) -> XCSourceEditorCommandInvocation? { guard let invocationClass = NSClassFromString("XCSourceEditorCommandInvocation") as? NSObject.Type else { return nil } let invocation = invocationClass.init() as! XCSourceEditorCommandInvocation // 关联属性到实例 objc_setAssociatedObject(invocation, &bufferAssocKey, buffer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_setAssociatedObject(invocation, &selectionAssocKey, selection, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) return invocation }
内容的提问来源于stack exchange,提问作者ADB
相关产品推荐
相关产品推荐

