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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 02:23:15