单元测试Alamofire app中的HTTP流量

时间:2014-11-13 21:18:46

标签: unit-testing alamofire

我正在努力弄清楚如何最好地测试使用Alamofire帮助同步服务器数据的应用。

我希望能够测试使用Alamofire的代码并处理来自服务器的JSON响应。 我想模拟那些测试,以便我可以将预期的响应数据提供给那些测试,而不会产生真正的网络流量。

这篇博客文章(http://nshipster.com/xctestcase/)描述了在Swift中模拟一个对象是多么容易 - 但我不知道如何用Alamofire及其链式响应来做到这一点。

我会嘲笑经理吗?要求?响应?任何帮助将不胜感激!

4 个答案:

答案 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")
    }