Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit Testing HTTP traffic in Alamofire app

I'm struggling a bit to figure out how to best test an app that uses Alamofire to help sync with server data.

I want to be able to test my code that uses Alamofire and processes JSON responses from a server. I'd like to mock those tests so that I can feed the expected response data to those tests without incurring real network traffic.

This blog post (http://nshipster.com/xctestcase/) describes how easy it is to Mock an object in Swift - but I'm not sure how to do that with Alamofire and its chained responses.

Would I mock the Manager? the Request? Response? Any help would be appreciated!

like image 610
Daniel D Avatar asked Nov 13 '14 21:11

Daniel D


3 Answers

Waiting for an answer by @mattt I post an example of my code.

Let's say that we have a Client class that is responsible for calling a simple web service. This class implements a function called userSignIn that performs a sign in using the WS.

This is the code for the userSignIn function:

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

The aim of the function is to get a token from the web service if the information passed by the user are correct.

The function checkSignInResponse (I don't report its code since it's not useful for the answer) has the role to valorise the 3 variables operationComplete, accessToken and error depending on the JSON response received.

Now that the 3 variables have a value we call the completionHandler using them.

How to mock this function?!

To mock the response I override the userSignIn function directly into the test function (as explained by the NSHipster article).

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

}

then I substitute the client inside the view controller that I'm testing using my "mocked" client. In this case I'm testing that the controller passes to the function information that are not valid so I check that the error property of the controller is not nil. To force this data I simply set operationComplete to false and I manual generate an NSError.

Does it make any sense to you? I'm not sure that this test is a good test... but at least I can verify the data flow.

like image 112
MatterGoal Avatar answered Nov 04 '22 10:11

MatterGoal


This question is getting old, but I just encountered the same issue, and the solution is very easy when using OHHTTPStubs.

OHHTTPStubs just mocks the responses you get from NSURLSession, so it works well with Alamofire, and you get very good coverage of your code path.

For example, in your test case, just mock the response using:

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)
})
like image 9
user541160 Avatar answered Nov 04 '22 08:11

user541160


I believe I have a solution to this for the newer versions of Alamofire. My Swift and DI skills are a bit noob so this can probably be improved but I thought I'd share. The most challenging part of mocking Alamofire is mocking the method chaining in the Network call (request().responseJSON).

The Network call:

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

The manager that you'll inject into the network call class: The NetworkManagerProtocol provides the get manager functionality to the various types of network managers.

class NetworkManager: NetworkManagerProtocol {
    private let sessionManager: NetworkManagerProtocol

    init(_ sessionManager: NetworkManagerProtocol) {
        self.sessionManager = sessionManager
    }

    var manager: SessionManagerProtocol {
        get {
            return sessionManager.manager
        }
        set {}
    }
}

Extend Alamofire's SessionManager class: This is where we add the protocols and custom functionality to SessionManager. Note the protocol's request method is a wrapper around Alamofire's 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
    }
}

Create a SessionManagerMock for the mock api call: This class creates a SessionManagerMock object and then retrieves the mock data with its request method.

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

Extend Alamofire's DataRequest class: And again, note the protocol's responseJSON class is a wrapper around DataRequests's responseJSON class.

extension DataRequest: DataRequestProtocol {
    func responseJSON(completionHandler: @escaping (DataResponse<Any>) -> Void) -> Self {
        return self.responseJSON(queue: nil, options: .allowFragments, completionHandler: completionHandler)
    }
}

DataRequestMock Class: This class stores the data for the mock request. It could be built out a little more (add request data, etc) but you get the idea.

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

The Protocol Droids:

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
}

The test method: A lot of improvements could be made to make this more dynamic but again you get the idea

    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": "[email protected]", "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")
    }
like image 1
Braden Holt Avatar answered Nov 04 '22 09:11

Braden Holt