In my app, I'm using Alamofire to make network calls, but now, I want to test the calls.
I do the network call like this :
AF.request(myURL).responseData { response in
// Decode the data
// Translate the data into an object
}
I tried to fake the AF session and override his request, but I don't know what should go inside.
class FakeAFSession: Session {
override func request(_ convertible: URLRequestConvertible, interceptor: RequestInterceptor? = nil) -> DataRequest {
// Fake the network request here
}
}
Here is how I want the test to run.
func testGivenCallbackFailed_WhenErrorInNetworkCall_ThenObjectReturnAreNil(){
// MARK: - Given
// I want to create a fake request
let fakeRequest = FakeNetworkRequest()
// I want to pass the request to the fake session of Alamofire
let fakeSession = FakeAFSession(request: fakeRequest)
// Finally, I want to pass the session to the service
let service = RecipeService(session: fakeSession)
// MARK: - When
let expectation = XCTestExpectation(description: "Error in network call")
let fakeIngredients = ["Mozzarella", "Tomato"]
service.getRecipe(containing: fakeIngredients) { _recipes, success, _error in
// MARK: - Then
// Here is where I should test the network call
XCTAssertNil(_recipes)
XCTAssertFalse(success)
XCTAssertNotNil(_error)
}
wait(for: [expectation], timeout: 0.01)
}
What technique should I use if this one is not recommended or the best ?
PS : I don't want to use only Xcode expectations to wait for the response,I really want to use faking or mocking process.
Thank's in advance for your answers.
Related
I am trying to work with a gRPC api and I have to send credentials securely. I am having issues to figure this out. I am using the swift-grpc library. I will link the docs and maybe someone can explain what I am supposed to do.
I am still unsure of what makes this secure through ssl(are we sending certificates).
docs from the swift-grpc using tlc library here
If anyone can give an explenation of how the ssl works and what to do that would be great
// code
import Foundation
import GRPC
import NIO
class Networking {
var authServiceClient: PartnerApi2_PartnerApiClient?
let port: Int = 50052
init() {
// build a fountain of EventLoops
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
do {
// open a channel to the gPRC server
let channel = try GRPCChannelPool.with(
target: .host("partner-api.com", port: self.port),
transportSecurity: .plaintext,
eventLoopGroup: eventLoopGroup
)
// create a Client
self.authServiceClient = PartnerApi2_PartnerApiClient.init(channel: channel) //AuthService_AuthServiceRoutesClient(channel: channel)
print("grpc connection initialized")
login(username: "email", password: "password")
} catch {
print("Couldn’t connect to gRPC server")
}
}
func login(username: String, password: String) -> String {
print("Login: username=\(username)")
// build the AccountCredentials object
let accountCredentials: PartnerApi2_AuthenticationRequest = .with {
$0.partnerEmail = username
$0.password = password
}
// grab the login() method from the gRPC client
let call = self.authServiceClient!.authenticate(accountCredentials)
// prepare an empty response object
let oauthCredentials: PartnerApi2_AuthenticationResponse
// execute the gRPC call and grab the result
do {
oauthCredentials = try call.response.wait()
} catch {
print("RPC method ‘login’ failed: \(error)")
// it would be better to throw an error here, but
// let’s keep this simple for demo purposes
return ""
}
// Do something interesting with the result
let oauthToken = oauthCredentials.authToken
print("Logged in with oauth token \(oauthToken)")
// return a value so we can use it in the app
return oauthToken
}
}
I'm trying to remove my dependency on RxAlamofire.
I currently have this function:
func requestData(_ urlRequest: URLRequestConvertible) -> Observable<(HTTPURLResponse, Data)> {
RxAlamofire.request(urlRequest).responseData()
}
How can I refactor this and use Alamofire directly to build and return an RxSwift Observable?
I suggest you look at the way the library wraps URLRequest to get an idea on how to do it...
Below is an abbreviated example from the library. In essence, you need to use Observable.create, make the network call passing in a closure that knows how to use the observer that create gives you.
Make sure you send a completed when done and make sure the disposable knows how to cancel the request.
Your Base will be something in Alamofire (I don't use Alamofire so I'm not sure what that might be.)
extension Reactive where Base: URLSession {
/**
Observable sequence of responses for URL request.
Performing of request starts after observer is subscribed and not after invoking this method.
**URL requests will be performed per subscribed observer.**
Any error during fetching of the response will cause observed sequence to terminate with error.
- parameter request: URL request.
- returns: Observable sequence of URL responses.
*/
public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable.create { observer in
let task = self.base.dataTask(with: request) { data, response, error in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
observer.on(.next((httpResponse, data)))
observer.on(.completed)
}
task.resume()
return Disposables.create(with: task.cancel)
}
}
}
The code within the function is executed in a different order than it is expected. I wanted to change the state of the login Boolean variable inside the if statement, but the function returns the initial value before if statement is completed.
Code sample:
class ClassName {
func loginRequest (name: String, pwd: String) -> Bool {
var login:Bool
//Initial value for login
login = false
let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
if let httpResponse = response as? HTTPURLResponse {
print(httpResponse.statusCode)
if (httpResponse.statusCode) == 200 {
//Change the value of login if login is successful
login = true
if let data = data, let dataString = String(data: data, encoding: .utf8) {
do {
...
} catch {print(error.localizedDescription)}
}
}
}
}
task.resume()
//Problem return false in any case because return is completed before if statement
return login
}
}
Completion Handlers is your friend
The moment your code runs task.resume(), it will run your uploadTask and only when that function is finished running it will run the code where you change your login variable.
With That said: That piece of code is running asynchronously. That means your return login line of code won't wait for your network request to come back before it runs.
Your code is actually running in the order it should. But i myself wrote my first network call like that and had the same problem. Completion Handles is how i fixed it
Here is a very nice tutorial on Completion Handlers or you might know it as Callbacks :
Link To Completion Handlers Tutorial
If i can give you a little hint - You will have to change your function so it looks something like this: func loginRequest (name: String, pwd: String, completionHandler: #escaping (Bool) -> Void)
And replace this login = true with completionHandler(true)
Wherever it is you call your function it will look something like this:
loginRequest(name: String, pwd: String) {didLogIn in
print("Logged In : \(didLogIn)")
}
One last thing... You're actually already using Completion Handlers in your code.
let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
... ... But hopefully now you understand a little bit better, and will use a completion handler approach when making network calls.
GOOD LUCK !
The API I use requires multiple requests to get search results. It's designed this way because searches can take a long time (> 5min). The initial response comes back immediately with metadata about the search, and that metadata is used in follow up requests until the search is complete. I do not control the API.
1st request is a POST to https://api.com/sessions/search/
The response to this request contains a cookie and metadata about the search. The important fields in this response are the search_cookie (a String) and search_completed_pct (an Int)
2nd request is a POST to https://api.com/sessions/results/ with the search_cookie appended to the URL. eg https://api.com/sessions/results/c601eeb7872b7+0
The response to the 2nd request will contain either:
The search results if the query has completed (aka search_completed_pct == 100)
Metadata about the progress of search, search_completed_pct is the progress of the search and will be between 0 and 100.
If the search is not complete, I want to make a request every 5 seconds until it's complete (aka search_completed_pct == 100)
I've found numerous posts here that are similar, many use Dispatch Groups and for loops, but that approach did not work for me. I've tried a while loop and had issues with variable scoping. Dispatch groups also didn't work for me. This smelled like the wrong way to go, but I'm not sure.
I'm looking for the proper design to make these recursive calls. Should I use delegates or are closures + loop the way to go? I've hit a wall and need some help.
The code below is the general idea of what I've tried (edited for clarity. No dispatch_groups(), error handling, json parsing, etc.)
Viewcontroller.swift
apiObj.sessionSearch(domain) { result in
Log.info!.message("result: \(result)")
})
ApiObj.swift
func sessionSearch(domain: String, sessionCompletion: (result: SearchResult) -> ()) {
// Make request to /search/ url
let task = session.dataTaskWithRequest(request) { data, response, error in
let searchCookie = parseCookieFromResponse(data!)
********* pseudo code **************
var progress: Int = 0
var results = SearchResults()
while (progress != 100) {
// Make requests to /results/ until search is complete
self.getResults(searchCookie) { searchResults in
progress = searchResults.search_pct_complete
if (searchResults == 100) {
completion(searchResults)
} else {
sleep(5 seconds)
} //if
} //self.getResults()
} //while
********* pseudo code ************
} //session.dataTaskWithRequest(
task.resume()
}
func getResults(cookie: String, completion: (searchResults: NSDictionary) -> ())
let request = buildRequest((domain), url: NSURL(string: ResultsUrl)!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { data, response, error in
let theResults = getJSONFromData(data!)
completion(theResults)
}
task.resume()
}
Well first off, it seems weird that there is no API with a GET request which simply returns the result - even if this may take minutes. But, as you mentioned, you cannot change the API.
So, according to your description, we need to issue a request which effectively "polls" the server. We do this until we retrieved a Search object which is completed.
So, a viable approach would purposely define the following functions and classes:
A protocol for the "Search" object returned from the server:
public protocol SearchType {
var searchID: String { get }
var isCompleted: Bool { get }
var progress: Double { get }
var result: AnyObject? { get }
}
A concrete struct or class is used on the client side.
An asynchronous function which issues a request to the server in order to create the search object (your #1 POST request):
func createSearch(completion: (SearchType?, ErrorType?) -> () )
Then another asynchronous function which fetches a "Search" object and potentially the result if it is complete:
func fetchSearch(searchID: String, completion: (SearchType?, ErrorType?) -> () )
Now, an asynchronous function which fetches the result for a certain "searchID" (your "search_cookie") - and internally implements the polling:
func fetchResult(searchID: String, completion: (AnyObject?, ErrorType?) -> () )
The implementation of fetchResult may now look as follows:
func fetchResult(searchID: String,
completion: (AnyObject?, ErrorType?) -> () ) {
func poll() {
fetchSearch(searchID) { (search, error) in
if let search = search {
if search.isCompleted {
completion(search.result!, nil)
} else {
delay(1.0, f: poll)
}
} else {
completion(nil, error)
}
}
}
poll()
}
This approach uses a local function poll for implementing the polling feature. poll calls fetchSearch and when it finishes it checks whether the search is complete. If not it delays for certain amount of duration and then calls poll again. This looks like a recursive call, but actually it isn't since poll already finished when it is called again. A local function seems appropriate for this kind of approach.
The function delay simply waits for the specified amount of seconds and then calls the provided closure. delay can be easily implemented in terms of dispatch_after or a with a cancelable dispatch timer (we need later implement cancellation).
I'm not showing how to implement createSearch and fetchSearch. These may be easily implemented using a third party network library or can be easily implemented based on NSURLSession.
Conclusion:
What might become a bit cumbersome, is to implement error handling and cancellation, and also dealing with all the completion handlers. In order to solve this problem in a concise and elegant manner I would suggest to utilise a helper library which implements "Promises" or "Futures" - or try to solve it with Rx.
For example a viable implementation utilising "Scala-like" futures:
func fetchResult(searchID: String) -> Future<AnyObject> {
let promise = Promise<AnyObject>()
func poll() {
fetchSearch(searchID).map { search in
if search.isCompleted {
promise.fulfill(search.result!)
} else {
delay(1.0, f: poll)
}
}
}
poll()
return promise.future!
}
You would start to obtain a result as shown below:
createSearch().flatMap { search in
fetchResult(search.searchID).map { result in
print(result)
}
}.onFailure { error in
print("Error: \(error)")
}
This above contains complete error handling. It does not yet contain cancellation. Your really need to implement a way to cancel the request, otherwise the polling may not be stopped.
A solution implementing cancellation utilising a "CancellationToken" may look as follows:
func fetchResult(searchID: String,
cancellationToken ct: CancellationToken) -> Future<AnyObject> {
let promise = Promise<AnyObject>()
func poll() {
fetchSearch(searchID, cancellationToken: ct).map { search in
if search.isCompleted {
promise.fulfill(search.result!)
} else {
delay(1.0, cancellationToken: ct) { ct in
if ct.isCancelled {
promise.reject(CancellationError.Cancelled)
} else {
poll()
}
}
}
}
}
poll()
return promise.future!
}
And it may be called:
let cr = CancellationRequest()
let ct = cr.token
createSearch(cancellationToken: ct).flatMap { search in
fetchResult(search.searchID, cancellationToken: ct).map { result in
// if we reach here, we got a result
print(result)
}
}.onFailure { error in
print("Error: \(error)")
}
Later you can cancel the request as shown below:
cr.cancel()
There are functions that send a request to server, get response and print result. They always work in the iOS app itself but only sometimes (looks like randomly) in unit-tests of this app.
Main issue: Xcode doesn't enter the body of a closure in unit-tests, just skips it.
Any ideas how can it be fixed? Image of the problem in Xcode.
The most likely reason because the completion closure of your requests are not being exectued is that they are performing an asynchronous operation, while the tests run synchronously. This means that the test finishes running while your network request is still processing.
Try using XCTestExpectation:
func testIt() {
let expectation = expectationWithDescription("foobar")
// request setup code here...
Alamofire.request(.POST, "...")
.responseJSON { response in
//
// Insert the test assertions here, for example:
//
if let JSON = response.result.value as? [String: AnyObject] {
XCTAssertEqual(JSON["id"], "1")
} else {
XCTFail("Unexpected response")
}
//
// Remember to call this at the end of the closure
//
expectation.fulfill()
}
//
// This will make XCTest wait for up to 10 seconds,
// giving your request expectation time to fulfill
//
waitForExpectationsWithTimeout(10) { error
if let error = error {
XCTFail("Error: \(error.localizedDescription)")
}
}
}