使用SwiftUI与Combine的应用如何实现ViewModel的Mock?
SwiftUI + Combine 环境下 Mock ViewModel 的实现方案
你遇到的@Published无法直接在协议中声明的问题,可以通过协议声明对应Publisher的方式绕开,完整实现方案如下:
1. 基于协议的完整Mock方案
该方案可以完全解耦View和具体ViewModel实现,同时支持单元测试验证方法调用、状态变更。
@Published 是属性包装器,本身无法直接定义在协议中,但我们可以在协议中声明对应属性的 Publisher 类型来满足依赖抽象的要求。
步骤1:定义ViewModel协议
协议必须继承ObservableObject,只需要声明对外暴露的属性、对应Publisher以及业务方法即可:
protocol UserViewModelProtocol: ObservableObject { // 对外可读写的状态属性 var username: String { get set } var isLoading: Bool { get set } // 对应@Published生成的Publisher var usernamePublisher: Published<String>.Publisher { get } var isLoadingPublisher: Published<Bool>.Publisher { get } // 业务方法 func fetchUserInfo() }
步骤2:让原业务ViewModel遵守协议
不需要修改原有业务代码,只需要扩展实现协议要求的Publisher属性即可,@Published修饰的属性会自动生成带$前缀的Publisher,直接返回即可:
// 原有真实业务ViewModel class UserViewModel: ObservableObject { @Published var username: String = "" @Published var isLoading: Bool = false func fetchUserInfo() { // 原有真实网络请求、业务逻辑 isLoading = true DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.isLoading = false self.username = "真实用户" } } } // 扩展遵守协议 extension UserViewModel: UserViewModelProtocol { var usernamePublisher: Published<String>.Publisher { $username } var isLoadingPublisher: Published<Bool>.Publisher { $isLoading } }
步骤3:实现Mock版本ViewModel
直接遵守协议,按需模拟状态和方法行为即可,还可以添加标记验证方法调用情况:
class MockUserViewModel: UserViewModelProtocol { @Published var username: String = "默认模拟用户" @Published var isLoading: Bool = false var usernamePublisher: Published<String>.Publisher { $username } var isLoadingPublisher: Published<Bool>.Publisher { $isLoading } // 标记方法是否被调用,用于单元测试验证 var fetchUserInfoIsCalled = false func fetchUserInfo() { fetchUserInfoIsCalled = true // 直接返回模拟结果,不需要执行真实逻辑 isLoading = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.isLoading = false self.username = "模拟请求返回的用户名" } } }
步骤4:View层适配
将View的ViewModel改为泛型,依赖协议而非具体实现:
struct UserView<T: UserViewModelProtocol>: View { @StateObject var viewModel: T var body: some View { VStack(spacing: 20) { if viewModel.isLoading { ProgressView("加载中") } else { Text("用户名:\(viewModel.username)") } Button("获取用户信息") { viewModel.fetchUserInfo() } } } } // 正式环境使用 // UserView(viewModel: UserViewModel()) // 测试/预览环境使用 // UserView(viewModel: MockUserViewModel())
轻量替代方案(仅适用于预览/简单Mock场景)
如果不需要做单元测试验证方法调用,只是为了SwiftUI预览快速构造假数据,可以直接继承原有ViewModel重写对应方法,不需要写协议:
class PreviewUserViewModel: UserViewModel { override func fetchUserInfo() { // 跳过真实请求,直接赋值模拟数据 self.isLoading = false self.username = "预览专用用户名" } } // 预览时使用 // UserView(viewModel: PreviewUserViewModel())
内容的提问来源于stack exchange,提问作者Edwin ZAP
相关产品推荐
相关产品推荐

