I want to test my network request code. Testing is not my strong point, and I'm doing this mainly to get better at creating testable code. I usually use Alamofire, but want to bypass that for unit tests. I have created a protocol like this:
protocol NetworkSession {
func request(
_ endpoint: Endpoint,
method: HTTPMethod,
completion: #escaping (Result<<DataResponse<Any>>, RequestError>) -> ()
)
}
I conform to this protocol in my class NetworkSessionManager:
final class NetworkSessionManager : NetworkSession {
func request(
_ endpoint: Endpoint,
method: HTTPMethod,
completion: #escaping (Result<<DataResponse<Any>>, RequestError>) -> ()) {
guard let url = endpoint.url else {
return completion(.failure(.couldNotEncode))
}
Alamofire.request(url, method: method, headers: headers)
.validate()
.responseJSON { response in
completion(.success(response))
}
}
}
}
Then in my BackendClient class I inject and call my NetworkSessionManager like this:
private let session: NetworkSession
init(session: NetworkSession = NetworkSessionManager()) {
self.session = session
}
func someFunction(...) {
session.request(...)
}
Now I know I need to create a mock class that conforms to NetworkSession, but I'm not sure what I should do in it? How do I test all the possibilities and returned data? There are different end points on my server that return different types of data. If someone could point me towards the next steps I should take, I'd be very grateful.
final class NetworkSessionManagerMock : NetworkSession {
func request(
_ endpoint: Endpoint,
method: HTTPMethod,
completion: #escaping (Result<<DataResponse<Any>>, RequestError>) -> ()) {
???
}
}
I'll show two examples that can hopefully give you something to start with in mocking out your NetworkSession.
First though, you can add additional properties to your mock class that support injecting values to test various cases. I will demonstrate an error situation and a return for now.
enum MyError: Swift.Error {
case failed
}
final class NetworkSessionManagerMock : NetworkSession {
var myError: MyError?
var dataResponse: Any? // This should hopefully be typed to something
func request(
_ endpoint: Endpoint,
method: HTTPMethod,
completion: #escaping (Result<<DataResponse<Any>>, RequestError>) -> ()) { [weak self] in
if let myError = self?.myError {
// Send error with completion handler
} else if let dataResponse = self?.dataResponse {
// Send data response
}
}
}
The idea here is that you instantiate the mock in your test, then set the variables as needed to test the respective part. Your mock will then proceed normally.
I did not test this code for syntax correctness though, so let me know if there is a mistake in it.
Related
I have this case where I need to make 3 nested async calls to receive the data I want.
So the second call needs data from the first one and the third one needs data from the second one. I do not have a lot of cases like this. Only this one and another one with only two nested call so I was thinking about a pure swift solution without any external libraries but I'm open to everything.
Since I'm using Firebase, is it better to move this logic to CloudFunctions? So to prepare it in the backend?
FirestoreService().fetchCollection(query: query) { (result: Result<[Request], Error>) in
// do stuff
FirestoreService().fetchCollection(query: query) { (result: Result<[Request], Error>) in
// do stuff
FirestoreService().fetchDocument(documentReference: documentReference) { (result: Result<Package, Error>) in
// finish
}
}
}
}
If you don't to used 3rd party library, then probably you want to consider wrap those operations inside some class, and utilise closure in imperative way.
here is the sample:
class CustomFirestoreHandler {
private var onFetchFirstQueryArrived: ((Result<[Request], Error>) -> ())? = nil
private var onFetchSecondQueryArrived: ((Result<[Request], Error>) -> ())? = nil
private var onFetchDocumentArrived: ((Result<Package, Error>) -> ())? = nil
init() {
onFetchFirstQueryArrived = { [weak self] (result: Result<[Request], Error>) in
self?.executeSecondQuery()
}
onFetchSecondQueryArrived = { [weak self] (result: Result<[Request], Error>) in
self?.executeFetchDocument()
}
}
func executeQuery(completion: #escaping (Result<Package, Error>) -> ()) {
self.onFetchDocumentArrived = completion
FirestoreService().fetchCollection(query: query) { [weak self] (result: Result<[Request], Error>) in
// validate if some error occurred and do early return here, so that we don't need necessarily call second query.
if (result.error == whatever) {
self?.onFetchDocumentArrived?(result)
return
}
self?.onFetchFirstQueryArrived?(result)
}
}
private func executeSecondQuery() {
FirestoreService().fetchCollection(query: query) { [weak self] (result: Result<[Request], Error>) in
// validate if some error occurred and do early return here, so that we don't need necessarily call fetch document.
if (result.error == whatever) {
self?.onFetchDocumentArrived?(result)
return
}
self?.onFetchSecondQueryArrived?(result)
}
}
private func executeFetchDocument() {
FirestoreService().fetchDocument(documentReference: documentReference) { (result: Result<Package, Error>) in
self?.onFetchDocumentArrived?(result)
}
}
}
And here's the usage of CustomFirestoreHandler above :
let firestoreHandler = CustomFirestoreHandler()
firestoreHandler.executeQuery { (result: Result<Package, Error>) in
// Handle `result` here...
}
I know it look complicated, but this is the only way I think (CMIIW) at the moment to prevent pyramid of dooms since swift doesn't have async await style(just like javascript does).
I use the following piece of code to generate a cold RxSwift Observable:
func doRequest<T :Mappable>(request:URLRequestConvertible) -> Observable<T> {
let observable = Observable<T>.create { [weak self] observer in
guard let self = self else { return Disposables.create() }
self.session.request(request).validate().responseObject { (response: AFDataResponse<T>) in
switch response.result {
case .success(let obj):
observer.onNext(obj)
observer.onCompleted()
case .failure(let error):
let theError = error as Error
observer.onError(theError)
}
}
return Disposables.create()
}
return observable
}
where Mappable is an ObjectMapper based type, and self.session is an Alamofire's Session object.
I can't find an equivalent to Observable.create {...} in Apple's Combine framework. What I only found is URLSession.shared.dataTaskPublisher(for:) which creates a publisher using Apple's URLSession class.
How can I convert the above observable to an Alamofire Combine's publisher ?
EDIT:
using the solution provided by rob, I ended up with the following:
private let apiQueue = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)
func doRequest<T>(request: URLRequestConvertible) -> AnyPublisher<T, AFError> where T : Mappable {
Deferred { [weak self] () -> Future<T, AFError> in
guard let self = self else {
return Future<T, AFError> { promise in
promise(.failure(.explicitlyCancelled)) }
}
return Future { promise in
self.session
.request(request)
.validate()
.responseObject { (response: AFDataResponse<T>) in
promise(response.result)
}
}
}
.handleEvents(receiveCompletion: { completion in
if case .failure (let error) = completion {
//handle the error
}
})
.receive(on: self.apiQueue)
.eraseToAnyPublisher()
}
EDIT2: I have to remove the private queue since it's not needed, Alamofire does the parsing the decoding on its own, so remove the queue and its usages (.receive(on: self.apiQueue))
You can use Future to connect responseObject's callback to a Combine Publisher. I don't have Alamofire handy for testing, but I think the following should work:
func doRequest<T: Mappable>(request: URLRequestConvertible) -> AnyPublisher<T, AFError> {
return Future { promise in
self.session
.request(request)
.validate()
.responseObject { (response: AFDataResponse<T>) in
promise(response.result)
}
}.eraseToAnyPublisher()
}
Note that this is somewhat simpler than the RxSwift version because promise takes a Result directly, so we don't have to switch over response.result.
A Future is sort of a “lukewarm” publisher. It is like a hot observable because it executes its body immediately and only once, so it starts the Alamofire request immediately. It is also like a cold observable, because every subscriber eventually receives a value or an error (assuming you eventually call promise). The Future only executes its body once, but it caches the Result you pass to promise.
You can create a truly cold publisher by wrapping the Future in a Deferred:
func doRequest<T: Mappable>(request: URLRequestConvertible) -> AnyPublisher<T, AFError> {
return Deferred {
Future { promise in
self.session
.request(request)
.validate()
.responseObject { (response: AFDataResponse<T>) in
promise(response.result) }
}
}.eraseToAnyPublisher()
}
Deferred calls its body to create a new inner Publisher every time you subscribe to it. So each time you subscribe, you'll create a new Future that will immediately start a new Alamofire request. This is useful if you want to use the retry operator, as in this question.
How can I mock URLSession.DataTaskPublisher? I have a class Proxy that require to inject a URLSessionProtocol
protocol URLSessionProtocol {
func loadData(from url: URL) -> URLSession.DataTaskPublisher
}
class Proxy {
private let urlSession: URLSessionProtocol
init(urlSession: URLSessionProtocol) {
self.urlSession = urlSession
}
func get(url: URL) -> AnyPublisher<Data, ProxyError> {
// Using urlSession.loadData(from: url)
}
}
This code was originally used with the traditional version of URLSession with the completion handler. It was perfect since I could easily mock URLSession for testing like Sundell's solution here: Mocking in Swift.
Is it possible to do the same with the Combine Framework?
In the same way that you can inject a URLSessionProtocol to mock a concrete session, you can also inject a mocked Publisher. For example:
let mockPublisher = Just(MockData()).eraseToAnyPublisher()
However, depending on what you do with this publisher you might have to address some weirdnesses with Combine async publishers, see this post for additional discussion:
Why does Combine's receive(on:) operator swallow errors?
The best way to test your client is to use URLProtocol.
https://developer.apple.com/documentation/foundation/urlprotocol
You can intercept all your request before she performs the real request on the cloud, and so creates your expectation. Once you have done your expectation, she will be destroyed, so you never make real requests.
Tests more reliable, faster, and you got the control!
You got a little example here: https://www.hackingwithswift.com/articles/153/how-to-test-ios-networking-code-the-easy-way
But it's more powerful than just this, you can do everything you want like: Check your Events/Analytics...
I hope it'll help you!
Since DataTaskPublisher uses the URLSession it is created from, you can just mock that. I ended up creating a URLSession subclass, overriding dataTask(...) to return a URLSessionDataTask subclass, which I fed with the data/response/error I needed...
class URLSessionDataTaskMock: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
class URLSessionMock: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with request: URLRequest, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}
}
Then obviously you just want your networking layer using this URLSession, I went with a factory to do this:
protocol DataTaskPublisherFactory {
func make(for request: URLRequest) -> URLSession.DataTaskPublisher
}
Then in your network layer:
func performRequest<ResponseType>(_ request: URLRequest) -> AnyPublisher<ResponseType, APIError> where ResponseType : Decodable {
Just(request)
.flatMap {
self.dataTaskPublisherFactory.make(for: $0)
.mapError { APIError.urlError($0)} } }
.eraseToAnyPublisher()
}
Now you can just pass a mock factory in the test using the URLSession subclass (this one asserts URLErrors are mapped to a custom error, but you could also assert some other condition given data/response):
func test_performRequest_URLSessionDataTaskThrowsError_throwsAPIError() {
let session = URLSessionMock()
session.error = TestError.test
let dataTaskPublisherFactory = mock(DataTaskPublisherFactory.self)
given(dataTaskPublisherFactory.make(for: any())) ~> {
session.dataTaskPublisher(for: $0)
}
let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
let publisher: AnyPublisher<TestCodable, APIError> =
api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
let _ = publisher.sink(receiveCompletion: {
switch $0 {
case .failure(let error):
XCTAssertEqual(error, APIError.urlError(URLError(_nsError: NSError(domain: "NSURLErrorDomain", code: -1, userInfo: nil))))
case .finished:
XCTFail()
}
}) { _ in }
}
The one issue with this is that URLSession init() is deprecated from iOS 13, so you have to live with a warning in your test. If anyone can see a way around that I'd greatly appreciate it.
(Note: I'm using Mockingbird for mocks).
I want to know how to pass data using closure. I know that there are three types of data pass approaches:
delegate
Notification Center
closure
I want proper clarification of closure with an example.
Well passing data with blocks / closures is a good and reasonable approach and much better than notifications.
Below is the same code for it.
First ViewController (where you make object of Second ViewController)
#IBAction func push(sender: UIButton) {
let v2Obj = storyboard?.instantiateViewControllerWithIdentifier("v2ViewController") as! v2ViewController
v2Obj.completionBlock = {[weak self] dataReturned in
//Data is returned **Do anything with it **
print(dataReturned)
}
navigationController?.pushViewController(v2Obj, animated: true)
}
Second ViewController (where data is passed back to First VC)
import UIKit
typealias v2CB = (infoToReturn :String) ->()
class v2ViewController: UIViewController {
var completionBlock:v2CB?
override func viewDidLoad() {
super.viewDidLoad()
}
func returnFirstValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: "any value")
}
}
This example explains use of service call with Alamofire and send the response back to calling View Controller with closure.
Code in Service Wrapper Class:
Closure declaration
typealias CompletionHandler = (_ response: NSDictionary?, _ statusCode: Int?, _ error: NSError?) -> Void
Closure implementation in method
func doRequestFor(_ url : String, method: HTTPMethod, dicsParams : [String: Any]?, dicsHeaders : [String: String]?, completionHandler:#escaping CompletionHandler) {
if !NetworkReachablity().isNetwork() {
return
}
if (dicsParams != nil) {print(">>>>>>>>>>>>>Request info url: \(url) --: \(dicsParams!)")}
else {print(">>>>>>>>>>>>>Request info url: \(url)")}
Alamofire.request(url, method: method, parameters: dicsParams, encoding:
URLEncoding.default, headers: dicsHeaders)
.responseJSON { response in
self.handleResponse(response: response, completionHandler: completionHandler)
}
}
Code at calling view controller:
ServiceWrapper().doRequestFor(url, method: .post, dicsParams: param, dicsHeaders: nil) { (dictResponse, statusCode, error) in
}
I am trying to get user data from a server. The application does not have to show any views until the data is loaded.
I read about typealias and I don't understand how to use it.
What I want: when data is loaded, move on to next step. If failed, load data again.
Here's how I declare typealias
typealias onCompleted = () -> ()
typealias onFailed = () -> ()
Here is my request code
func getUserData(_ completed: #escaping onCompleted, failed: #escaping onFailed){
let fullURL = AFUtils.getFullURL(AUTHURL.getUserData)
AFNetworking.requestGETURL(fullURL, params: nil, success: {
(JSONResponse) -> Void in
if let status = JSONResponse["status"].string {
switch status{
case Status.ok:
completed()
break
default:
failed()
break
}
}
})
}
But how could I use this on my view controller when calling getUserData?
Assuming your custom AFNetworking.requestGETURLs completion handler is called on the main queue:
func viewDidLoad() {
super.viewDidLoad()
getUserData({
//do somthing and update ui
}) {
//handle error
}
}
Edit:
How I understand your comment, you actually want to name your completion and error block parameters. If so, change the method to :
func getUserData(completion completed: #escaping onCompleted, error failed: #escaping onFailed){ ... }
and call it like this:
getUserData(completion: {
//do somthing and update ui
}, error: {
//handle error
})