Mocking URLSession to return a Mocked URLSessionDataTask - swift

I want to mock URLSession, and return a mocked URLSessionDataTask.
To Mock URLSession I create a protocol
protocol URLSessionProtocol {
func dataTask(with url: URL, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}
that URLSession can then conform to in an extension
extension URLSession: URLSessionProtocol {}
Now I want to do the same for URLSessionDataTask, and so implement a similar protocol and extension for it. I need to do this, since the way I call URLSession requires use of func dataTask(with url: URL, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
protocol URLSessionDataTaskProtocol {
func resume()
}
extension URLSessionDataTask: URLSessionDataTaskProtocol {}
So then my URLSessionDataTask mock is set up as follows:
class URLSessionMock: URLSessionProtocol {
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
// data and error can be set to provide data or an error
var data: Data?
var error: Error?
func dataTask(
with url: URL,
completionHandler: #escaping CompletionHandler
) -> URLSessionDataTask {
let data = self.data
let error = self.error
return URLSessionDataTaskMock {
completionHandler(data, nil, error)
}
}
}
With my URLSessionDataTaskMock presented with:
class URLSessionDataTaskMock: URLSessionDataTaskProtocol {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
// override resume and call the closure
func resume() {
closure()
}
}
Doesn't work since URLSessionDataTaskMock within the URLSessionProtocol isn't the correct return type - I need to return a URLSessionDataTask.
I can't cast my URLSessionDataTaskMock to URLSessionDataTask as the types are not related.
How can I return my URLSessionDataTaskMock from my URLSessionProtocol?

You can probably get away with something like this. The key would be your associated type in your URLSessionProtocol
protocol URLSessionProtocol {
associatedtype DataTaskType
func dataTask(with url: URL, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> DataTaskType
}
extension URLSession: URLSessionProtocol {}
protocol URLSessionDataTaskProtocol {
func resume()
}
extension URLSessionDataTask: URLSessionDataTaskProtocol {}
class URLSessionDataTaskMock: URLSessionDataTaskProtocol {
typealias CompletionHandler = URLSessionMock.CompletionHandler
private let completion: CompletionHandler
init(completion: #escaping CompletionHandler) {
self.completion = completion
}
func resume() {
// create some data
completion(nil, nil, nil)
}
}
class URLSessionMock: URLSessionProtocol {
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
// data and error can be set to provide data or an error
var data: Data?
var error: Error?
func dataTask(
with url: URL,
completionHandler: #escaping CompletionHandler
) -> URLSessionDataTaskMock {
return URLSessionDataTaskMock(completion: completionHandler)
}
}

Related

is it possible swift func get <T> enum?

there's two func get enum from parameter,
is it possible those func combined just one func?
func rushdownSetupListener(event: RushdownListenEvent, handler: #escaping ([Any], SocketAckEmitter) -> Void) -> UUID? {
guard let client = self.client else {
return nil
}
let listenerId = client.on(event.rawValue, callback: handler)
return listenerId
}
func hystoriaSetupListener(event: HystoriaListenEvent,handler: #escaping ([Any], SocketAckEmitter) -> Void) -> UUID? {
guard let client = client else {
return nil
}
let listenerId = client.on(event.rawValue, callback: handler)
return listenerId
}
Since both of those enums have a String as their raw value, they both conform to RawRepresentable where RawValue == String. Therefore, you can introduce a generic parameter type constrained to exactly that:
func setupListener<EventType>(event: EventType, handler: #escaping ([Any], SocketAckEmitter) -> Void) -> UUID?
where EventType: RawRepresentable, EventType.RawValue == String {
guard let client = self.client else {
return nil
}
let listenerId = client.on(event.rawValue, callback: handler)
return listenerId
}
You should be able to even simplify the function body to just:
client?.on(event.rawValue, callback: handler)

Capturing closures within closures: Xcode throws error: Escaping closure captures non-escaping parameter

I am trying to capture a closure inside another closure (to use it return the response), but when I do this (case 2), I get an error. My intention is to use second inside the response of a URLSession, but unfortunately I cannot.
Case 1: It works (just printing, because there is no inner closure)
import UIKit
class Callback {
// let callback: (String, (String?) -> Void) -> Void
let callback: (String, String) -> Void
public init(callback: #escaping ((String, String) -> Void)) {
self.callback = callback
}
}
Callback { first, second in
let url = URL(string: "https://testapi.com")!
URLSession.init().dataTask(with: url) { data, response, error in
print(second)
}
}
Case 2: Error. Escaping closure captures non-escaping parameter 'second'
import UIKit
class NestedCallback {
// let callback: (String, (String?) -> Void) -> Void
let callback: (String, () -> Void) -> Void
public init(callback: #escaping ((String, () -> Void) -> Void)) {
self.callback = callback
}
}
NestedCallback { first, second in
let url = URL(string: "https://testapi.com")!
URLSession.init().dataTask(with: url) { data, response, error in <---- error: Escaping closure captures non-escaping parameter 'second'
second()
}
}
I've simplified the code, but in reality I will use second as way to send data with an argument. (second will have 2 parameters, data and error)
I did consider adding another #escaping declaration for the nested callback, but this is not allowed.
The following compiles fine in Swift 5.4:
import UIKit
class NestedCallback {
let callback: (String, #escaping () -> Void) -> Void
public init(callback: #escaping ((String, #escaping () -> Void) -> Void)) {
self.callback = callback
}
}
NestedCallback(callback: { first, second in
let url = URL(string: "https://testapi.com")!
URLSession.init().dataTask(with: url) { data, response, error in
second()
}
})

How would you test this generic API?

I've got the following APIRequest protocol:
protocol APIRequest {
associatedtype Result
var httpMethod: HTTPMethod { get }
var pathComponents: [String] { get }
func handle(responseData: Data) throws -> Result
}
Additionally, I've got the following APIClient protocol:
protocol APIClientProtocol {
func perform<T : APIRequest>(_ request: T,
completion: #escaping ((Result<T.Result, APIError>) -> Void))
}
Then I've got a class which takes an APIClientProtocol and make the request. IE:
final class DataSource {
let client: APIClientProtocol
init(client: APIClientProtocol) {
self.client = client
}
func fetchThing(completion: #escaping (Result<Thing, APIError>) -> Void) {
let thingRequest = ThingRequest() // This is an APIRequest
client.perform(thingRequest, completion: { result in
switch result {
case .success(let thing):
completion(.success(thing))
case .failure(let error):
completion(.failure(error))
}
}
}
Now I want to write a test for DataSource but I need to mock APIClientProtocol in order to do that. How can I mock it?
Using a mock of URLProtocol is thinking too deep.
There are two basic ways to mock API.
Create a full mock server and connect to it instead of your real server. This way you can create integration tests.
Just mock the response directly
class APIClientProtocolMock: APIClientProtocol {
func perform<T : APIRequest>(
_ request: T,
completion: #escaping ((Result<T.Result, APIError>) -> Void)
) {
// replace with a background queue depending on your implementation
DispatchQueue.main.async {
completion(.success(/* put your mock response here */))
}
}
}
To be honest, I am not a big friend of unit testing on FE with extensive mocking because e2e tests (UI tests) do a much better job and you don't have to create hundreds of unnecessary protocols for mocking to work.
A real example:
protocol APIRequest {
associatedtype Result
var httpMethod: String { get }
var pathComponents: [String] { get }
func handle(responseData: Data) throws -> Result
}
protocol APIClientProtocol {
func perform<T : APIRequest>(
_ request: T,
completion: #escaping ((Result<T.Result, Error>) -> Void)
)
}
struct MockAPIRequest: APIRequest {
typealias Result = String
let httpMethod: String = "POST"
let pathComponents: [String] = ["url"]
func handle(responseData: Data) throws -> Result {
return "success"
}
}
struct SecondMockAPIRequest: APIRequest {
typealias Result = String
let httpMethod: String = "POST"
let pathComponents: [String] = ["url"]
func handle(responseData: Data) throws -> Result {
return "failed"
}
}
class MockAPIClientProtocol: APIClientProtocol {
var mockData: [String: Data] = [:]
// you can load mock data dynamically
func perform<T : APIRequest>(_ request: T, completion: #escaping ((Result<T.Result, Error>) -> Void)) {
let data = mockData[request.pathComponents.joined(separator: "/")] ?? Data()
completion(.success(try! request.handle(responseData: data)))
}
// you can add implementation for a specific type
func perform(_ request: SecondMockAPIRequest, completion: #escaping ((Result<SecondMockAPIRequest.Result, Error>) -> Void)) {
let data = mockData[request.pathComponents.joined(separator: "/")] ?? Data()
completion(.success(try! request.handle(responseData: data)))
}
}
let client = MockAPIClientProtocol()
client.mockData["url"] = Data()
client.perform(MockAPIRequest()) { result in
print(result) // success
}
client.perform(SecondMockAPIRequest()) { result in
print(result) // failed
}

ambiguous reference to member datatask swift Xcode 8

I just updated my Xcode and have been trying to learn more about it. This is my code for Collection View Cell. I'm trying to get data from URL but I've been annoyed with this error. I tried all the solutions in here but they have different function structure. Also other solutions didn't work.
import UIKit
class PersonCell: UICollectionViewCell {
#IBOutlet weak var personImage : UIImageView!
func ConfigureCell (imgURL : String)
{
}
func DownloadImage ( url : NSURL)
{
}
func GetDatafromURL (URL : NSURL , completion : #escaping (( _ data : NSData? , _ response : URLResponse? , _ error : NSError?) -> Void))
{
URLSession.shared.dataTask(with: URL) { (data , response , error) in
completion (data , response, error)
}.resume()
}
}
the code that worked in the tutorial video is something like this
func GetDatafromURL (URL : NSURL , completion : (( data : NSData? , response : NSURLResponse? , error : NSError?) -> Void))
{
NSURLSession.sharedSession.dataTaskWithURL( URL) { (data , response , error) in
completion(data : data , responce : response, error : error)
}.resume()
You can convert your NSURL to a URL using .absoluteURL
guard let url = URL.absoluteURL else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
//
}.resume()
UPDATE
Your completion block has the wrong types. You're using NSData instead of Data and NSError instead of Error. Here's an example of what it should look like below.
func getData(from url: URL, completion: #escaping (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void)
{
URLSession.shared.dataTask(with: url) { data, response, error
completion(data, response, error)
}.resume()
}
Please check the latest reference.
func dataTask(with: URL, completionHandler: (Data?, URLResponse?, Error?) -> Void)
Declaration
func dataTask(with url: URL,
completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
In Swift 3, the completionHandler of dataTask(with:completionHandler:) takes 3 arguments with types Data?, URLResponse? and Error?. So, your method's completion, which is of type (NSData?,URLResponse?,NSError?) -> Void cannot be applied.
And it takes URL as its first parameter.
(Thanks to vadian, he suggested all needed things here.)
So, your GetDatafromURL needs to be something like this:
func GetDatafromURL(url: URL, completion: #escaping ((_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void))
{
URLSession.shared.dataTask(with: url) { (data, response, error) in
completion (data , response, error)
}.resume()
}

Swift 2.0 : Cannot invoke with an argument list of type... (HTTP Request)

Since I upgraded to Xcode 7 beta I have an error that I can't fix.
Here's the full code from my DataManager.swift
import Foundation
var TopAppURL:String = String()
var numberAsked:String = String()
class DataManager {
class func getInfo(ID : String){
TopAppURL = "http://sweetapi.com/?=\(ID)"
numberAsked = ID
}
class func loadDataFromURL(url: NSURL, completion:(data: NSData?, error: NSError?) -> Void) {
var session = NSURLSession.sharedSession()
// Use NSURLSession to get data from an NSURL
let loadDataTask = session.dataTaskWithURL(url, completionHandler: { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
if let responseError = error {
completion(data: nil, error: responseError)
} else if let httpResponse = response as? NSHTTPURLResponse {
if httpResponse.statusCode != 200 {
var statusError = NSError(domain:"com.raywenderlich", code:httpResponse.statusCode, userInfo:[NSLocalizedDescriptionKey : "HTTP status code has unexpected value."])
completion(data: nil, error: statusError)
} else {
completion(data: data, error: nil)
}
}
})
loadDataTask.resume()
}
class func getDataFromSweetApiOk(success: ((IDdata: NSData!) -> Void)) {
//1
print("DataManager loads \(TopAppURL)")
loadDataFromURL(NSURL(string: TopAppURL)!, completion:{(data, error) -> Void in
//2
if let urlData = data {
//3
success(IDdata: urlData)
}
})
}
}
So I got this error : "Cannot invoke 'dataTaskWithURL' with an argument list of type '(NSURL, completionHandler: (NSData!, NSURLResponse!, NSError!) -> Void)'"
I searched everywhere how to fix this but like Swift 2.0 is very new, I didn't found any solution.
func dataTaskWithURL(_ url: NSURL,
completionHandler completionHandler: ((NSData!,
NSURLResponse!,
NSError!) -> Void)?) -> NSURLSessionDataTask
has changed to
func dataTaskWithURL(_ url: NSURL,
completionHandler completionHandler: (NSData?,
NSURLResponse?,
NSError?) -> Void) -> NSURLSessionDataTask?
in iOS9. The completionHandler no longer is optional, and all parameters in the completionHandler are now optionals instead of implicitly unwrapped optionals.
Now, to help with this in future changes to the optional system, try to avoid (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in, you can simply use data, response, error in and then option-click for more details.
This will remove bloat from your code, and thus improve readability.
To solve your problem in the comments, check out this question.