简体   繁体   中英

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

Once I will be able I will award 50 bounty for the one, who help me do this.

Writing mock tests for AFNetworking is unfortunetaly not helpful. It is not working for me.

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 S ubject U nder T est(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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM