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) } }
额外优化建议
- 确保
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 } } - 避免在ViewModel的
init中直接启动异步任务,可以改成暴露loadPosts方法,让测试主动触发,这样测试流程更可控
内容的提问来源于stack exchange,提问作者Nupur Rawat
相关产品推荐
相关产品推荐

