在 iOS 业务开发过程经常面对网络请求,数据持久化这样带有副作用的操作。为了能够在测试中 mock 这些操作,通常的做法就是抽象一层 protocol 出来,然后编写不同的实现。
比如需要处理一个简陋的注册业务(示例省略了一点细节),需要用户输入信息后发送网络请求,成功后返回对应用户对象。
首先为网络请求定义一个 protocol:
protocol SignUpRepositoryProtocol: RepositoryProtocol { func handleSignUp(name: String, email: String, pwd: String) -> AnyPublisher<User, Error> } 对其进行实现:
struct SignUpRepository: SignUpRepositoryProtocol { func handleSignUp(name: String, email: String, pwd: String) -> AnyPublisher<User, Error> { client(.signUp(name, email, pwd)) .map .decode .eraseToAnyPublisher() } } 将其注入到 ViewModel 或是 Interactor 中(取决于你采取的架构是什么 :p ),并且调用对应方法:
class SignUpViewModel { enum State { case loading case success case failed } let repository: SignUpRepositoryPortocol var state: State = .loading init(repository: SignUpRepositoryPortocol) { self.repository = repository } func onSubmit(name: String, email: String, pwd: String) { repository.hanldeSignUp(name: name, email: email, pwd: pwd) .sink {[weak self] completion in switch completion { case .failure: self.?state = .failed case .finished: break } } receiveValue: {[weak self] result in self?.state = .success }.store(in: &bag) } } 由此如果需要测试对应的方法,只需要再创建一份 MockSignUpRepository 的实现即可,比如想要测试注册成功或失败场景下的处理:
struct MockSignUpRepository: SignUpRepositoryPortocol { let shouldSignUpSuccess: Bool func handleSignUp(name: String, email: String, pwd: String) -> AnyPublisher<User, Error> { if shouldSignUpSuccess { Just(User.mock) .mapError{ _ in SignUpError.someError } .eraseToAnyPublisher() } else { Fail(error: SignUpError.someError) .eraseToAnyPublisher() } } } 在编写测试的时候,传入 SignUpViewModel 的依赖替换成我们想要测试的 Mock 实现:
...