我正在努力弄清楚如何最好地测试使用Alamofire帮助同步服务器数据的应用。
我希望能够测试使用Alamofire的代码并处理来自服务器的JSON响应。 我想模拟那些测试,以便我可以将预期的响应数据提供给那些测试,而不会产生真正的网络流量。
这篇博客文章(http://nshipster.com/xctestcase/)描述了在Swift中模拟一个对象是多么容易 - 但我不知道如何用Alamofire及其链式响应来做到这一点。
我会嘲笑经理吗?要求?响应?任何帮助将不胜感激!
答案 0 :(得分:21)
我正在添加另一个答案,因为我刚刚发现这种方法在我看来更容易阅读和使用。
我创建了一个虚拟Alamofire类,它只包含测试所需的函数和类型。 现在我将此文件包含在测试目标中而不是真正的Alamofire中。
例如,我创建了Request
类的版本,我根据测试定义了几个静态变量,对于这个类,我只实现了init
以及responseJSON
功能。
public class Request {
var request:String?
struct response{
static var data:NSHTTPURLResponse?
static var json:AnyObject?
static var error:NSError?
}
init (request:String){
self.request = request
}
public func responseJSON(options: NSJSONReadingOptions = .AllowFragments, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self {
completionHandler(NSURLRequest(URL: NSURL(string:self.request!)!), Request.response.data, Request.response.json, Request.response.error)
return self
}
}
现在我可以在测试中模拟一个响应:
func testMytestFunction(){
var HTMLResponse = NSHTTPURLResponse(URL: NSURL(string: "myurl")!, statusCode: 200, HTTPVersion: "HTTP/1.1", headerFields: nil)
Request.response.data = HTMLResponse
Request.response.json = LoadDataFromJSONFile("MyJsonFile")
request(.POST, "myurl", parameters: nil, encoding: ParameterEncoding.JSON).responseJSON {
(request, response, JSON, error) -> Void in
// the JSON and response variable now contains exactly the data that you have passed to Request.response.data and Request.response.json
}
}
请求函数在此处定义:
public func request(method: Method, URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL) -> Request {
return Request(request: URLString.URLString)
}
public func request(URLRequest: URLRequestConvertible) -> Request {
return Request(request: "fakecall")
}
答案 1 :(得分:9)
这个问题已经老了,但我遇到了同样的问题,使用OHHTTPStubs时解决方案非常简单。
OHHTTPStubs只是嘲笑你从NSURLSession得到的回复,所以它适用于Alamofire,你可以很好地覆盖你的代码路径。
例如,在您的测试用例中,只需使用以下方法模拟响应:
OHHTTPStubs.stubRequestsPassingTest({
(request: NSURLRequest) -> Bool in
return request.URL!.host == "myhost.com"
}, withStubResponse: {
(request: NSURLRequest) -> OHHTTPStubsResponse in
let obj = ["status": "ok", "data": "something"]
return OHHTTPStubsResponse(JSONObject: obj, statusCode:200, headers:nil)
})
答案 2 :(得分:1)
等待@mattt的回答我发布了我的代码示例。
我们假设我们有一个Client
类负责调用一个简单的Web服务。该类实现了一个名为userSignIn
的函数,该函数使用WS执行登录。
这是userSignIn
函数的代码:
func userSignIn(
#email:String,
password:String,
completionHandler: (Bool, String?, NSError?) -> Void
)-> Void
{
var parameters:[String:AnyObject] = [
"email":email,
"password":password,
]
Alamofire.request(.POST, Client.urlPath, parameters: parameters, encoding: ParameterEncoding.JSON).responseJSON {
(request, response, JSON, responseError) -> Void in
// Setup callback params
// HERE WE INJECT THE "FAKE" DATA--------
var operationComplete = false
var accessToken:String?
var error:NSError?
// --------------------------------------
if let statusCode = response?.statusCode {
// Check for errors and build response data
(operationComplete, accessToken, error) = self.checkSignInResponse(statusCode, JSON: JSON)
}
// Call the completion handler
completionHandler(operationComplete, accessToken, error)
}
}
该功能的目的是,如果用户传递的信息正确,则从Web服务获取令牌。
函数checkSignInResponse
(我不会报告其代码,因为它对答案没用)可以使3个变量operationComplete
,{{1} }和accessToken
取决于收到的JSON响应。
现在3个变量都有一个值,我们使用它们调用error
。
如何模拟此功能?!
为了模拟响应,我将completionHandler
函数直接覆盖到测试函数中(如NSHipster文章所述)。
userSignIn
然后我用视图控制器中的func testUserSignIn_whenParamsAreInvalid(){
class MockClient:Client {
override func userSignIn(#email: String, password: String, completionHandler:
(Bool, String?, NSError?) -> Void) {
// Set callback params
var operationComplete = false
var accessToken:String? = nil
var error:NSError? = NSError(domain: "Testing", code: 99, userInfo: nil)
completionHandler(operationComplete, accessToken, error)
}
}
signInViewController!.client = MockClient()
signInViewController!.loadView()
fillRegisterFieldsWithDataAndSubmit(femail(), password: fpassword())
XCTAssertNotNil(signInViewController!.error, "Expect error to be not nil")
}
替换我使用我的" mocked"进行测试。客户。在这种情况下,我测试控制器传递给无效的功能信息,所以我检查控制器的client
属性是不是nil。要强制使用此数据,我只需将error
设置为false,然后手动生成operationComplete
。
对你有意义吗?我不确定这个测试是一个很好的测试......但至少我可以验证数据流。
答案 3 :(得分:0)
我相信我对较新版本的Alamofire有解决方案。我的Swift和DI技能有些菜鸟,因此可能可以改善,但我想我会分享。模拟Alamofire最具挑战性的部分是模拟Network调用(request()。responseJSON)中的方法链接。
网络通话:
let networkManager: NetworkManagerProtocol!
init(_ networkManager: NetworkManagerProtocol = NetworkManagerTest(SessionManager())) {
self.networkManager = networkManager
}
func create(_ params: [String: Any], completion: @escaping (Response<Success,Fail>) -> Void) {
self.networkManager.manager.request(self.url!, method: .post, parameters: params, encoding: URLEncoding.default, headers: nil).responseJSON {
response in
if response.result.isSuccess {
completion(Success())
} else {
completion(Fail())
}
}
}
您要插入到网络通话类别中的经理:
NetworkManagerProtocol为各种类型的网络管理器提供了get manager
功能。
class NetworkManager: NetworkManagerProtocol {
private let sessionManager: NetworkManagerProtocol
init(_ sessionManager: NetworkManagerProtocol) {
self.sessionManager = sessionManager
}
var manager: SessionManagerProtocol {
get {
return sessionManager.manager
}
set {}
}
}
扩展了Alamofire的SessionManager类: 这是我们在SessionManager中添加协议和自定义功能的地方。请注意,协议的request方法是Alamofire的request方法的包装。
extension SessionManager: NetworkManagerProtocol, SessionManagerProtocol {
private static var _manager = SessionManager()
var manager: SessionManagerProtocol {
get {
return SessionManager._manager
}
set {
let configuration = URLSessionConfiguration.default
SessionManager._manager = Alamofire.SessionManager(configuration: configuration, delegate: SessionManager.default.delegate)
}
}
func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequestProtocol {
let dataRequest: DataRequest = self.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers)
return dataRequest
}
}
为模拟api调用创建SessionManagerMock: 此类创建SessionManagerMock对象,然后使用其request方法检索模拟数据。
class SessionManagerMock: NetworkManagerProtocol, SessionManagerProtocol {
private static var _manager = SessionManagerMock()
var manager: SessionManagerProtocol {
get {
return SessionManagerMock._manager
}
set {}
}
func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequestProtocol {
return DataRequestMock()
}
}
扩展了Alamofire的DataRequest类: 再次注意,协议的responseJSON类是DataRequests的responseJSON类的包装。
extension DataRequest: DataRequestProtocol {
func responseJSON(completionHandler: @escaping (DataResponse<Any>) -> Void) -> Self {
return self.responseJSON(queue: nil, options: .allowFragments, completionHandler: completionHandler)
}
}
DataRequestMock类: 此类存储模拟请求的数据。可以将其构建得更多(添加请求数据等),但您会明白的。
class DataRequestMock: DataRequestProtocol {
static var statusCode: Int = 200
var dataResponse = DataResponse<Any>(
request: nil,
response: HTTPURLResponse(url: URL(string: "foo.baz.com")!, statusCode: DataRequestMock.statusCode, httpVersion: "1.1", headerFields: nil),
data: nil,
result: Result.success(true), // enum
timeline: Timeline()
)
func response(completionHandler: @escaping (DataResponse<Any>) -> Void) -> Self {
completionHandler(dataResponse)
return self
}
func responseJSON(completionHandler: @escaping (DataResponse<Any>) -> Void) -> Self {
return response(completionHandler: completionHandler)
}
}
协议机器人:
protocol NetworkManagerProtocol {
var manager: SessionManagerProtocol { get set }
}
protocol SessionManagerProtocol {
func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequestProtocol
}
protocol DataRequestProtocol {
func responseJSON(completionHandler: @escaping (DataResponse<Any>) -> Void) -> Self
}
测试方法: 可以进行很多改进以使其更加动态,但是您再次有了主意
var sut: UserService?
override func setUp() {
super.setUp()
sut = UserService(NetworkManagerTest(SessionManagerMock()))
}
func testCreateUser201() {
DataRequestMock.statusCode = 201
let params : [String : String] = ["name": "foo baz", "email": "foobaz@gmail.com", "password": "tester123"]
var resultCode: Int!
sut?.create(params) {(response: Response) in
switch response {
case .success(let resp):
resultCode = resp.statusCode
case .failure(let resp):
resultCode = resp.statusCode
}
}
XCTAssertEqual(resultCode, 201, "Status code is wrong")
}