Search code examples
iosswiftunit-testingnetwork-programmingocmock

How to mock response from AFNetworking with OCMock in Swift?


This is how I get the instance of my network client:

let networkClient = DBNetworkClient(baseURL: NSURL(string: "http://mysite.pl/api"))

I also have one method:

func citiesWithParameters(parameters: [String: String], completionBlock: DBSearchOptionHandler) {
    
    GET("cities", parameters: parameters, success: { operation, response in
        
        if let error = NSError(response: response) {
            completionBlock([], error)
        } else {
            let cities = DBCity.parseCitiesWithDictionary(response as! NSDictionary)
            completionBlock(cities, nil)
        }
        
        }) { operation, error in
            
            completionBlock([], error)
    }
}

This is how I call this method:

networkClient.citiesWithParameters(parameters, completionBlock: { cities, error in 
    //do sth 
})

This way I pass some parameters, and get the real response from server. I would like to mock THAT response when I ask for this. How to do this?

func testCities() {

    let mockNetworkClient = OCMockObject.mockForClass(DBNetworkClient.classForCoder())        


    //what should I perform here to be able to do sth like this:

    let expectation = expectationWithDescription("")

    mockNetworkClient.citiesWithParameters(["a": "b"]) { cities, error in
        expectation.fulfill()
        XCTAssertNotNil(cities)
        XCTAssertNil(error) //since I know what is the response, because I mocked this
    }

    waitForExpectationsWithTimeout(10, handler: nil)
}

And this is how my method GET is defined within DBNetworkClient:

override func GET(URLString: String!, parameters: AnyObject!, success: ((AFHTTPRequestOperation!, AnyObject!) -> Void)!, failure: ((AFHTTPRequestOperation!, NSError!) -> Void)!) -> AFHTTPRequestOperation! {
    
    return super.GET(URLString, parameters: parameters, success: { (operation, response) in
        
        print("GET \(operation.request.URL)")
        print("GET \(response)")
        
        success(operation, response)
        
        }, failure: { (operation, error) in
            
            print("GET \(operation.request.URL)")
            print("GET \(operation.responseObject)")
            
            failure(operation, error)
    })
}

This article: Writing mock tests for AFNetworking is unfortunately not helpful. It is not working for me.


Solution

  • I do not have experience with Alamofire therefore I don't know where is declared your GET method but you should definitely stub this method instead of citiesWithParameters.

    For example if it's declared in your DBNetworkClient:

    func testCities()
    {
        //1
        class DBNetworkClientMocked: DBNetworkClient
        {
            //2
            override func GET(URLString: String!, parameters: AnyObject!, success: ((AFHTTPRequestOperation!, AnyObject!) -> Void)!, failure: ((AFHTTPRequestOperation!, NSError!) -> Void)!) -> AFHTTPRequestOperation! {
    
                //3
                success(nil, ["San Francisco", "London", "Sofia"])
    
            }
        }
    
        //4
        let sut = DBNetworkClientMocked()
        sut.citiesWithParameters(["a":"b"]) { cities, error in
    
            //5
            XCTAssertNotNil(cities)
            XCTAssertNil(error)
    
        }
    }
    

    So what happens here:

    1. You define class that is children to DBNetworkClient, therefore making a 'Mock' for it as described in article you posted. In that class we will override only methods that we want to change(stub) and leave others unchanged. This was previously done with OCMock and was called Partial Mock.
    2. Now you stub it's GET method in order to return specific data, not actual data from the server
    3. Here you can define what to return your stubbed server. It can be success failure etc.
    4. Now after we are ready with mocks and stubs, we create so called Subject Under Test(sut). Please note that if DBNetworkClient has specific constructor you must call this constructor instead of default one - ().
    5. We execute method that we want to test. Inside it's callback we put all our assertions.

    So far so good. However if GET method is part of Alamofire you need to use a technique called Dependency Injection(you could google it for more info).

    So if GET is declared inside another class and is only referenced in citiesWithParameters, how we can stub it? Let's look at this:

    //1
    class DBNetworkClient
    {
        //2
        func citiesWithParameters(parameters: [String: String], networkWorker: Alamofire = Alamofire(), completionBlock: DBSearchOptionHandler) {
    
            //3
            networkWorker.GET("cities", parameters: parameters, success: { operation, response in
    
                if let error = NSError(response: response) {
                    completionBlock([], error)
                } else {
                    let cities = DBCity.parseCitiesWithDictionary(response as! NSDictionary)
                    completionBlock(cities, nil)
                }
    
                }) { operation, error in
    
                    completionBlock([], error)
            }
    
        }
    }
    
    func testCities()
    {
        //4
        class AlamofireMock: Alamofire
        {
            override func GET(URLString: String!, parameters: AnyObject!, success: ((AFHTTPRequestOperation!, AnyObject!) -> Void)!, failure: ((AFHTTPRequestOperation!, NSError!) -> Void)!) -> AFHTTPRequestOperation! {
    
                success(nil, ["San Francisco", "London", "Sofia"])
    
            }
        }
    
        //5
        let sut = DBNetworkClient()
        sut.citiesWithParameters(["a":"b"], networkWorker: AlamofireMock()) { cities, error in
    
            //6
            XCTAssertNotNil(cities)
            XCTAssertNil(error)
    
        }
    }
    
    1. First we have to slightly change our citiesWithParameters to receive one more parameter. This parameter is our dependency injection. It is the object that have GET method. In real life example it will be better this to be only protocol as citiesWithParameters doesn't have to know anything more than this object is capable of making requests.
    2. I've set networkWorker parameter a default value, otherwise you need to change all your call to citiesWithParameters to fulfill new parameters requirement.
    3. We leave all the implementation the same, just now we call our injected object GET method
    4. Now back in tests, we will mock Alamofire this time. We made exactly the same thing as in previous example, just this time mocked class is Alamofire instead of DBNetworkClient
    5. When we call citiesWithParameters we pass our mocked object as networkWorker. This way our stubbed GET method will be called and we will get our expected data from 'fake' server.
    6. Our assertions are kept the same

    Please note that those two examples do not use OCMock, instead they rely entirely on Swift power! OCMock is a wonderful tool that we used in great dynamic language - Objective-C. However on Swift dynamism and reflection are almost entirely missing. Thats why even on official OCMock page we had following statement:

    Will there be a mock framework for Swift written in Swift? Maybe. As of now it doesn't look too likely, though, because mock frameworks depend heavily on access to the language runtime, and Swift does not seem to provide any.

    The thing that is missing with in both implementations provided is verifying GET method is called. I'll leave it like this because that was not original question about, and you can easily implement it with a boolean value declared in mocked class.

    One more important thing is that I assume GET method in Alamofire is instance method, not a class method. If that's not true you can declare new method inside DBNetworkClient which simply calls Alamofire.GET and use that method inside citiesWithParameters. Then you can stub this method as in first example and everything will be fine.