34

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

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

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

我会嘲笑经理吗?请求?回复?任何帮助,将不胜感激!

4

4 回答 4

23

我正在添加另一个答案,因为我刚刚发现这种方法在我看来更容易并且非常易于阅读和使用。

我创建了一个虚拟 Alamofire 类,它只包含测试所需的函数和类型。现在我将这个文件包含在测试目标中,而不是真正的 Alamofire。

例如,我创建了我的Request类版本,在其中我定义了几个静态变量,我根据测试来评估这些变量,并且对于这个类,我只实现了initandresponseJSON函数。

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")
}
于 2015-04-30T10:50:04.377 回答
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)
})
于 2015-05-22T22:06:58.327 回答
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具体取决于收到的 JSON 响应。accessTokenerror

现在这 3 个变量都有一个值,我们称之为completionHandlerusing 它们。

如何模拟这个功能?!

为了模拟响应,我将userSignIn函数直接覆盖到测试函数中(如 NSHipster 文章所述)。

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")

}

然后我client用我的“模拟”客户端替换我正在测试的视图控制器内部。在这种情况下,我正在测试控制器是否传递给无效的函数信息,因此我检查error控制器的属性是否不为零。为了强制这个数据,我简单地设置operationComplete为 false 并手动生成一个NSError.

这对你有意义吗?我不确定这个测试是不是一个很好的测试……但至少我可以验证数据流。

于 2015-04-21T15:37:34.913 回答
1

我相信我对新版本的 Alamofire 有一个解决方案。我的 Swift 和 DI 技能有点菜鸟,所以这可能可以改进,但我想我会分享。模拟 Alamofire 最具挑战性的部分是模拟网络调用中的方法链接(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 method 的包装器。

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
    }
}

为 mock api 调用创建一个 SessionManagerMock: 该类创建一个 SessionManagerMock 对象,然后使用其请求方法检索模拟数据。

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")
    }
于 2019-11-15T05:05:30.680 回答