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

Swift XCUnitTest访问SUT对象时因PassthroughSubject崩溃

iOS单元测试异步逻辑崩溃问题解决

问题核心原因

测试方法是同步执行的,而PostsViewViewModel初始化时会启动异步Task加载数据。测试方法执行完毕后立刻触发tearDown,销毁了sutPostViewModel,但sink的闭包此时还没执行,后续访问已释放的对象就会崩溃。

解决方案步骤

  • 用XCTestExpectation等待异步操作完成,确保闭包执行完毕后再结束测试方法
  • 每个测试方法单独维护自己的cancellable,避免跨测试干扰
  • 用可选绑定替代强制解包,避免空值访问崩溃
  • 确保Mock网络请求能同步返回结果(如果你的MockNetworkRequestPostSuccess是异步的,要改成同步实现)

修改后的测试代码示例

final class PostViewViewModelTest: XCTestCase {
    private let mockUser = LoginUserModel(userid: 1)
    private let coreDataManager = CoreDataStackInMemory()
    private var sutPostViewModel: PostsViewViewModel!
    
    override func setUp() {
        super.setUp()
        sutPostViewModel = PostsViewViewModel(
            request: MockNetworkRequestPostSuccess(),
            user: mockUser,
            codeDataManager: coreDataManager
        )
    }
    
    override func tearDown() {
        sutPostViewModel = nil
        super.tearDown()
    }
    
    func testPostsViewViewModel_WhenPostModelLoaded_NumberOfRowsSouldBeMoreThanZero()  {
        // 创建期望对象,等待异步回调
        let expectation = self.expectation(description: "Posts loaded successfully")
        
        // 每个测试单独创建cancellable,避免跨测试干扰
        var cancellables = Set<AnyCancellable>()
        
        sutPostViewModel.requestOutput
            .sink { [weak self] output in
                guard let self = self else {
                    expectation.fulfill()
                    return
                }
                
                XCTAssertEqual(output, .fetchPostsDidSucceed)
                // 用可选绑定替代强制解包
                if let rowCount = self.sutPostViewModel?.numberOfRowsInPostTableView {
                    XCTAssertTrue(rowCount > 0)
                } else {
                    XCTFail("Failed to get row count")
                }
                
                // 完成期望,通知测试可以结束
                expectation.fulfill()
            }
            .store(in: &cancellables)
        
        // 等待期望完成,超时时间设为合理值(比如2秒)
        waitForExpectations(timeout: 2) { error in
            if let error = error {
                XCTFail("Wait failed with error: \(error)")
            }
        }
    }
    
    func testPostsViewViewModel_WhenPostModelLoaded_GetPostAtGivenIndexPathMustHaveEqualPostID() {
        let expectation = self.expectation(description: "Post at index loaded successfully")
        var cancellables = Set<AnyCancellable>()
        
        sutPostViewModel.requestOutput
            .receive(on: DispatchQueue.main)
            .sink { [weak self] output in
                guard let self = self else {
                    expectation.fulfill()
                    return
                }
                
                // 确认输出类型
                guard output == .fetchPostsDidSucceed else {
                    XCTFail("Unexpected output: \(output)")
                    expectation.fulfill()
                    return
                }
                
                // 获取指定位置的post
                if let post = self.sutPostViewModel?.getPost(at: IndexPath(row: 0, section: 0)) {
                    let postModel: [PostModel] = JSONLoader.load("Posts.json")
                    XCTAssertEqual(post.postID, postModel[0].id)
                } else {
                    XCTFail("Failed to get post at index 0")
                }
                
                expectation.fulfill()
            }
            .store(in: &cancellables)
        
        waitForExpectations(timeout: 2)
    }
}

额外优化建议

  1. 确保MockNetworkRequestPostSuccess的callService方法是同步返回结果的,这样测试不会因为异步延迟超时
    class MockNetworkRequestPostSuccess: NetworkRequestProtocol {
        func callService<T>(with endPoint: ServiceEndPoint, model: T.Type, serviceMethod: ServiceMethod) async throws -> T where T : Decodable, T : Encodable {
            // 直接返回模拟数据,同步执行
            let mockPosts: [PostModel] = [PostModel(id: 1, userId: 1, title: "Test", body: "Test body")]
            return mockPosts as! T
        }
    }
    
  2. 避免在ViewModel的init中直接启动异步任务,可以改成暴露loadPosts方法,让测试主动触发,这样测试流程更可控

内容的提问来源于stack exchange,提问作者Nupur Rawat

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 20:01:33