iOS 基于 RxSwift + Moya 搭建易测试的网络请求
2019-11-20 本文已影响0人
FicowShen
内容概览
- Moya
- RxSwift
- 实例讲解
- 总结
Moya
- TargetType
/// The protocol used to define the specifications necessary for a `MoyaProvider`.
public protocol TargetType {
/// The target's base `URL`.
var baseURL: URL { get }
/// The path to be appended to `baseURL` to form the full `URL`.
var path: String { get }
/// The HTTP method used in the request.
var method: Moya.Method { get }
/// Provides stub data for use in testing.
var sampleData: Data { get }
/// The type of HTTP task to be performed.
var task: Task { get }
/// The type of validation to perform on the request. Default is `.none`.
var validationType: ValidationType { get }
/// The headers to be used in the request.
var headers: [String: String]? { get }
}
在遵循这个协议的类型中提供这些属性,可以很方便地定制API请求的各种参数。
而且,由于是属性,所以非常容易进行测试。
- MoyaProvider
/// Request provider class. Requests should be made through this class only.
open class MoyaProvider<Target: TargetType>: MoyaProviderType {
/// Closure that defines the endpoints for the provider.
public typealias EndpointClosure = (Target) -> Endpoint
/// Closure that decides if and what request should be performed.
public typealias RequestResultClosure = (Result<URLRequest, MoyaError>) -> Void
/// Closure that resolves an `Endpoint` into a `RequestResult`.
public typealias RequestClosure = (Endpoint, @escaping RequestResultClosure) -> Void
/// Closure that decides if/how a request should be stubbed.
public typealias StubClosure = (Target) -> Moya.StubBehavior
/// A closure responsible for mapping a `TargetType` to an `EndPoint`.
public let endpointClosure: EndpointClosure
/// A closure deciding if and what request should be performed.
public let requestClosure: RequestClosure
/// A closure responsible for determining the stubbing behavior
/// of a request for a given `TargetType`.
public let stubClosure: StubClosure
...
/// Initializes a provider.
public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
callbackQueue: DispatchQueue? = nil,
manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(),
plugins: [PluginType] = [],
trackInflights: Bool = false) {
self.endpointClosure = endpointClosure
self.requestClosure = requestClosure
self.stubClosure = stubClosure
self.manager = manager
self.plugins = plugins
self.trackInflights = trackInflights
self.callbackQueue = callbackQueue
}
...
}
实际的请求由MoyaProvider
发起,而实例化一个MoyaProvider
需要提供一个泛型参数Target
。
所以MoyaProvider
需要相应的TargetType
,然后才能完成请求。
在进行测试的时候,我们可以使用这些Closure
去深度定制MoyaProvider
实例以实现我们想要的效果。
RxSwift
基于RxSwift去实现网络请求层的优点:
-
函数式编程
,处理数据的方式更优雅(RxSwift内置许多操作符,封装了许多常用的处理逻辑) - 使用 Scheduler 切换线程,安全、高效、简洁
- 为以后全项目实现 MVVM with RxSwift 打下基础
初次接触 RxSwift 的朋友可以简单了解一下:
Learn & Master ⚔️ the Basics of RxSwift in 10 Minutes
实例讲解
Demo 地址:MoyaWithRxSwiftDemo
- 实现API,并遵循
TargetType
协议
/// 定义首页模块需要用到的API信息
struct HomeAPI {
let baseURL: URL
let endpoint: HomeAPIEndpoint
}
/// 多个API请求
enum HomeAPIEndpoint {
// 如果需要传递参数,可以使用枚举的关联值,比如:case basicInfo(userID: String)
case basicInfo
case hobbies
}
/// 遵循 TargetType 协议,并定制请求参数
extension HomeAPI: TargetType {
var path: String {
switch endpoint {
case .basicInfo:
return "basic_info.json"
case .hobbies:
return "hobbies.json"
}
}
var method: Moya.Method {
// 如果有必要,可以根据 endpoint 来改变
return .get
}
/// 每个请求需要用到的参数
var task: Task {
// 如果endpoint中的枚举有关联值,可以使用 switch 中的 case let 取出关联值
return .requestParameters(parameters: [:], encoding: URLEncoding.queryString)
}
var headers: [String : String]? { nil }
/// 不建议将测试数据放到App所在的Target
var sampleData: Data { Data() }
/// 测试时,如果需要测试状态码,就需要重写这个属性。
/// TargetType 协议中的默认实现是返回 .none,也就是不校验 statusCode
var validationType: ValidationType { .successAndRedirectCodes }
}
- 使用
MoyaProvider
发起请求,并处理回调(采用Observable
实现)
final class HomeNetworkHelper {
private let baseURL: URL
private let moyaProvider: MoyaProvider<HomeAPI>
// moyaProvider 采用依赖注入的方式进行初始化,便于测试
init(baseURL: URL,
moyaProvider: MoyaProvider<HomeAPI> = MoyaProvider<HomeAPI>()) {
self.baseURL = baseURL
self.moyaProvider = moyaProvider
}
func fetchBasicInfo() -> Observable<UserBasicInfo> {
return Observable.create { (observer) -> Disposable in
let api = HomeAPI(baseURL: self.baseURL, endpoint: .basicInfo)
self.requestAPI(api: api, observer: observer)
return Disposables.create()
}.observeOn(SerialDispatchQueueScheduler(qos: .default)) // 切换到后台线程
}
func fetchHobbies() -> Observable<UserHobbies> {
return Observable.create { (observer) -> Disposable in
let api = HomeAPI(baseURL: self.baseURL, endpoint: .hobbies)
self.requestAPI(api: api, observer: observer)
return Disposables.create()
}.observeOn(SerialDispatchQueueScheduler(qos: .default))
}
func requestAPI<T: Decodable>(api: HomeAPI, observer: AnyObserver<T>) {
// moyaProvider 发起网络请求
self.moyaProvider.request(api) { (response) in
switch response {
case .success(let value):
do {
let result = try value.map(T.self, atKeyPath: nil, using: JSONDecoder(), failsOnEmptyData: false)
observer.onNext(result)
observer.onCompleted()
} catch {
observer.onError(error)
}
case .failure(let error):
observer.onError(error)
}
}
}
}
- 订阅 Observable,处理网络请求回调
class ViewController: UIViewController {
...
private lazy var networkHelper = HomeNetworkHelper(baseURL: baseURL)
private let baseURL = URL(string: "https://raw.githubusercontent.com/FicowShen/MoyaWithRxSwiftDemo/master/MoyaWithRxSwiftDemo/json/")!
private let disposeBag = DisposeBag()
...
func loadFirstRow() {
networkHelper
.fetchBasicInfo()
.delay(DispatchTimeInterval.milliseconds(500),
scheduler: MainScheduler.instance)
.observeOn(MainScheduler.instance) // 切换到主线程。如果引入了RxCocoa,可以将Observable转换为Driver
.subscribe(onNext: { [unowned self] (model) in
self.models.insert(model, at: 0)
self.myTableView.reloadData()
}, onError: { (error) in
logError(error)
}).disposed(by: disposeBag)
}
}
- 对网络请求层进行单元测试
import XCTest
import Moya
import RxBlocking
@testable import MoyaWithRxSwiftDemo
class MoyaWithRxSwiftDemoTests: XCTestCase {
func testHomeAPI() {
guard let url = URL(string: "https://apple.com") else {
XCTFail()
return
}
var api = HomeAPI(baseURL: url, endpoint: .basicInfo)
XCTAssertEqual(api.path, "basic_info.json")
api = HomeAPI(baseURL: url, endpoint: .hobbies)
XCTAssertEqual(api.path, "hobbies.json")
}
/// 测试请求成功的情况
func testSuccessfulHomeAPIRequest() {
// 从项目中的JSON里读取数据用来模拟请求成功后返回的model数据
guard let url = URL(string: "https://apple.com"),
let basicInfoData = loadDataInJSONFile(fileName: "basic_info") else {
XCTFail()
return
}
// endpointClosure 可以用来修改
// 如果 MoyaProvider 的 stubClosure 不为空,则 MoyaProvider 不会发起真的网络请求
let provider = MoyaProvider<HomeAPI>(endpointClosure: { self.mockEndpointForAPI(api: $0, response: .networkResponse(200, basicInfoData)) },
stubClosure: { _ in .immediate })
let apiHelper = HomeNetworkHelper(baseURL: url, moyaProvider: provider)
// RxBlocking 提供了便捷的方法,可以阻塞住当前线程,然后收集 Observable 中的事件
guard let basicInfo = try? apiHelper.fetchBasicInfo().toBlocking().first() else {
XCTFail()
return
}
XCTAssertEqual(basicInfo.name, "John")
XCTAssertEqual(basicInfo.age, 10)
}
/// 测试发生网络故障(请求超时等情况)的情况
func testNetworkErrorForHomeAPIRequest() {
guard let url = URL(string: "https://apple.com") else {
XCTFail()
return
}
let expectedError = NSError(domain: "expectedError", code: -1, userInfo: nil)
let provider = MoyaProvider<HomeAPI>(endpointClosure: { self.mockEndpointForAPI(api: $0, response: .networkError(expectedError)) },
stubClosure: { _ in .immediate })
let apiHelper = HomeNetworkHelper(baseURL: url, moyaProvider: provider)
expectError(expectedError) {
_ = try apiHelper.fetchBasicInfo().toBlocking().first()
}
}
/// 测试请求成功后,响应为错误的情况
func testResponseErrorForHomeAPIRequest() {
guard let url = URL(string: "https://apple.com") else {
XCTFail()
return
}
let expectedError = NSError(domain: "", code: 404, userInfo: nil)
let provider = MoyaProvider<HomeAPI>(endpointClosure: {
self.mockEndpointForAPI(api: $0, response: .networkResponse(404, Data())) },
stubClosure: { _ in .immediate })
let apiHelper = HomeNetworkHelper(baseURL: url, moyaProvider: provider)
expectError(expectedError) {
_ = try apiHelper.fetchBasicInfo().toBlocking().first()
}
}
func mockEndpointForAPI(api: TargetType, response: EndpointSampleResponse) -> Endpoint {
return Endpoint(url: api.baseURL.absoluteString,
sampleResponseClosure: { response },
method: api.method,
task: api.task,
httpHeaderFields: api.headers)
}
func loadDataInJSONFile(fileName: String) -> Data? {
let bundle = Bundle(for: type(of: self))
guard let filePath = bundle.path(forResource: fileName, ofType: "json"),
let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
return nil
}
return data
}
func expectError(_ expectedError: NSError, inFailedRequest requestOperation: (() throws -> ())) {
do {
try requestOperation()
XCTFail()
} catch let error as Moya.MoyaError {
switch error {
case let .underlying(error as NSError, response):
if let response = response {
XCTAssertEqual(expectedError.code, response.statusCode)
} else {
XCTAssertEqual(error.code, expectedError.code)
}
default:
XCTFail()
}
} catch {
XCTFail()
}
}
}
总结
基于 RxSwift
+ Moya
搭建的网络请求层,具有以下优势:
- 可以模块化,易于扩展
- 职责分明,易于测试
- 函数式编程,操作简洁、顺畅
- 线程切换,简洁、高效
劣势:
-
RxSwift
学习成本略高 -
Moya
基于Alamofire
,需要引入一个网络请求库
参考文章:
Moya Tutorial for iOS: Getting Started
Getting Started With RxSwift and RxCocoa
RxSwift + MVVM: how to feed ViewModels
Testing Your RxSwift Code
转载请注明出处,谢谢~