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

Swift单元测试:无法修改Main Actor隔离属性的解决方案咨询

解决Main Actor隔离导致的单元测试编译错误

问题背景

你实现了标记为@MainActor的AcmeModel,在单元测试中直接修改events属性时遇到编译错误:

Main actor-isolated property 'events' can not be mutated from a non-isolated context

这是因为@MainActor将AcmeModel的所有属性和方法隔离到主执行上下文,而默认的异步测试方法运行在非隔离环境,无法直接访问主Actor隔离的成员。

可行解决方案

方案1:用MainActor.run包裹主Actor隔离操作

在测试中,通过MainActor.run将修改events和调用upcoming()的逻辑调度到主Actor执行:

func testUpcomingFiltersFutureAndOrdersNextUp() async {
    // 初始化模型(@MainActor标记的类,默认init在主Actor上下文,无需await)
    let candidate = AcmeModel()
    
    // 在主Actor上下文修改events
    await MainActor.run {
        // 构造测试用的Event数组,包含过去、现在、未来的事件
        let pastEvent = Event(date: Date().addingTimeInterval(-86400)) // 1天前
        let upcomingEvent1 = Event(date: Date().addingTimeInterval(3600)) // 1小时后
        let upcomingEvent2 = Event(date: Date().addingTimeInterval(7200)) // 2小时后
        candidate.events = [pastEvent, upcomingEvent2, upcomingEvent1]
    }
    
    // 在主Actor上下文调用upcoming()并获取结果
    let upcomingEvents = await MainActor.run { candidate.upcoming() }
    
    // 断言逻辑:仅返回未来事件,且按升序排列(注意:你的upcoming()方法当前是降序,需和注释对齐)
    XCTAssertEqual(upcomingEvents.count, 2)
    XCTAssertEqual(upcomingEvents[0].date, Date().addingTimeInterval(3600))
    XCTAssertEqual(upcomingEvents[1].date, Date().addingTimeInterval(7200))
}

方案2:添加测试友好的初始化方法

给AcmeModel新增一个可直接传入events的初始化方法,避免在测试中手动修改属性:

@MainActor
public class AcmeModel: ObservableObject {
    @Published var events: [Event] = []
    
    // 测试用初始化方法
    public init(events: [Event]) {
        self.events = events
    }
    
    // 默认初始化方法
    public init() {}
    
    func upcoming() -> [Event] {
        events
            .filter { $0.date >= Date() }
            .sorted{ $0.date < $1.date } // 修正:这里改为<才是升序,和注释匹配
    }
}

测试代码可简化为:

func testUpcomingFiltersFutureAndOrdersNextUp() async {
    let pastEvent = Event(date: Date().addingTimeInterval(-86400))
    let upcomingEvent1 = Event(date: Date().addingTimeInterval(3600))
    let upcomingEvent2 = Event(date: Date().addingTimeInterval(7200))
    let testEvents = [pastEvent, upcomingEvent2, upcomingEvent1]
    
    // 在主Actor上下文初始化模型
    let candidate = await MainActor.run { AcmeModel(events: testEvents) }
    
    let upcomingEvents = await MainActor.run { candidate.upcoming() }
    
    // 执行断言
    XCTAssertEqual(upcomingEvents.count, 2)
    XCTAssertEqual(upcomingEvents.first?.date, upcomingEvent1.date)
}

方案3:将测试方法标记为@MainActor

如果测试逻辑简单,可直接将整个测试方法标记为@MainActor,让测试代码运行在主Actor上下文:

@MainActor
func testUpcomingFiltersFutureAndOrdersNextUp() {
    let candidate = AcmeModel()
    
    let pastEvent = Event(date: Date().addingTimeInterval(-86400))
    let upcomingEvent1 = Event(date: Date().addingTimeInterval(3600))
    let upcomingEvent2 = Event(date: Date().addingTimeInterval(7200))
    candidate.events = [pastEvent, upcomingEvent2, upcomingEvent1]
    
    let upcomingEvents = candidate.upcoming()
    
    XCTAssertEqual(upcomingEvents.count, 2)
    XCTAssertEqual(upcomingEvents[0].date, upcomingEvent1.date)
}

注意:这种方式会让测试运行在主Actor,可能影响测试并行执行效率,适合单例或依赖主Actor的简单测试。

额外注意点

你的upcoming()方法注释说明返回升序排列的事件,但当前代码用sorted{ $0.date > $1.date }实现的是降序,需要根据需求修正逻辑,否则测试断言会失败。


内容的提问来源于stack exchange,提问作者Adrian Föder

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 23:50:16