I have the following ClassA and ClassB in swift:
protocol ClassBProtocol {
func doSomething(
completionHandler: #escaping (
ClassBProtocol?,
Error?
) -> Void
)
}
class ClassB: ClassBProtocol
{
init(key: String) {
self.key = key
}
func doSomething(
completionHandler: #escaping (
ClassBProtocol?,
Error?
) -> Void
) {
// Does some network requests and if it was successful does the following:
completionHandler(ClassB(), nil)
}
}
public class ClassA: ClassAProtocol {
static var instance: ClassA? = nil
static let initQueue = DispatchQueue(label: "queue")
static let semaphore = DispatchSemaphore(value: 1)
#objc public static func getSomething(
withKey key: String,
completionHandler: #escaping (ClassA?, Error?) -> Void
) {
ClassA.initQueue.async {
ClassA.semaphore.wait()
DispatchQueue.main.async {
if let objectA = ClassA.instance {
ClassA.semaphore.signal()
completionHandler(objectA, nil)
return
}
let objectB = ClassB(withKey: key)
objectB.doSomething { response, error in
guard let response = response else {
ClassA.semaphore.signal()
completionHandler(nil, error)
return
}
let objectA = ClassA()
ClassA.instance = objectA
ClassA.semaphore.signal()
completionHandler(objectA, nil)
}
}
}
}
}
And the following test case to test ClassA.getSomething() and ensure a race condition does not happen:
func testgetSomethingReturnsSameInstance() {
let expectation1 = self.expectation(description: "getSomething 1 completed")
let expectation2 = self.expectation(description: "getSomething 2 completed")
let expectation3 = self.expectation(description: "getSomething 3 completed")
var client1: ClassA?
var client2: ClassA?
var client3: ClassA?
ClassA.getSomething() { (client, error) in
client1 = client
expectation1.fulfill()
}
ClassA.getSomething() { (client, error) in
client2 = client
expectation2.fulfill()
}
ClassA.getSomething() { (client, error) in
client3 = client
expectation3.fulfill()
}
waitForExpectations(timeout: 10) { (error) in
XCTAssertEqual(client1, client2)
XCTAssertEqual(client2, client3)
XCTAssertEqual(client1, client3)
}
}
The doSomething in ClassB sends a network request and will return an object. I need to mock this method to do the following:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completionHandler(response: ClassB(), error: nil)
}
But couldn't find any way to do it. Does anyone have a solution for this?
It'd be easier to understand your code if you used reasonable names. I suspect ClassB wraps an API, and ClassA holds some value (like a database) that you want to fetch from the API exactly once. So let's rename ClassBProtocol and ClassB accordingly:
protocol API {
func fetch(completion: #escaping (Result<Data, Error>) -> Void)
}
class LiveAPI: API {
let key: String
init(key: String) {
self.key = key
}
func fetch(completion: #escaping (Result<Data, Error>) -> Void) {
// Do some network requests to get the real `Data` or a real error, and then:
completion(.success(Data()))
// or in case of error:
// completion(.failure(error))
}
}
Then let's rename ClassA to Database and put getSomething on hold, leaving just this:
public class Database: NSObject {
private init(data: Data) {
// construct myself from the raw data
}
}
Now, how to fetch the database exactly once, the first time it's requested?
The general advice from Apple engineers is to avoid potentially blocking operations in blocks you send to Dispatch queues. In this case, semaphore.wait() is a potentially blocking operation.
Furthermore, synchronous code is easier to test than asynchronous code, but you've made everything asynchronous. The first thing your getSomething does is an async dispatch, and a significant chunk of state (the set of pending completion handlers) is hidden away in Dispatch data structures that we cannot access.
Instead of using semaphore and initQueue, let's manually and synchronously track the completion handlers that need to be called when the database has been fetched. There are three states:
We haven't started fetching the database.
We've started fetching the database, but it's still downloading and we have one or more completion handlers to be called when it's finished.
We've finished fetching the database and there are no completion handlers to call.
We'll store these three mutually-exclusive states using an enum, and guard access to the stored state using a DispatchQueue:
extension Database {
// All access to q_fetchState must be on q!
private static var q_fetchState: FetchState = .unstarted
private static let q = DispatchQueue(label: "initQueue")
private typealias Completion = (Result<Database, Error>) -> Void
private enum FetchState {
case unstarted
case started([Completion])
case done(Result<Database, Error>)
}
}
When asked for the database, we examine the state and act appropriately:
extension Database {
#objc
public static func getDatabase(
apiKey key: String,
completion objc_completion: #escaping (Database?, Error?) -> Void
) {
let completion: Completion = {
switch $0 {
case .failure(let error): objc_completion(nil, error)
case .success(let database): objc_completion(database, nil)
}
}
q.sync {
switch q_fetchState {
case .unstarted:
q_fetchState = .started([completion])
DispatchQueue.main.async {
let api = LiveAPI(key: key)
api.fetch { result in
let result = result.map { Database(data: $0) }
let completions = q.sync {
guard case .started(let completions) = q_fetchState else {
preconditionFailure()
}
q_fetchState = .done(result)
return completions
}
for completion in completions {
completion(result)
}
}
}
case .started(let array):
q_fetchState = .started(array + [completion])
case .done(let result):
DispatchQueue.main.async {
completion(result)
}
}
}
}
}
Notice that there are no blocking operations performed under q, so it's safe and efficient to use q.sync instead of q.async, and we'll see later that it makes the function more testable.
Okay, now back to your actual question, which I interpret as: How do we mock the API? Since we already have an API protocol, we want to make getDatabase generic over a type conforming to API, and make it take an instance of that type:
extension Database {
static func getDatabase<A: API>(
api: A,
completion: #escaping (Result<Database, Error>) -> Void
) {
q.sync {
switch q_fetchState {
case .unstarted:
q_fetchState = .started([completion])
DispatchQueue.main.async {
api.fetch { result in
let result = result.map { Database(data: $0) }
let completions = q.sync {
guard case .started(let completions) = q_fetchState else {
preconditionFailure()
}
q_fetchState = .done(result)
return completions
}
for completion in completions {
completion(result)
}
}
}
case .started(let array):
q_fetchState = .started(array + [completion])
case .done(let result):
DispatchQueue.main.async {
completion(result)
}
}
}
}
}
These changes mean the method is no longer compatible with Objective-C. So let's add an overload with the old, Objective-C-compatible signature:
#objc
public static func getDatabase(
apiKey key: String,
completion: #escaping (Database?, Error?) -> Void
) {
return getDatabase(api: LiveAPI(key: key)) {
switch $0 {
case .failure(let error): completion(nil, error)
case .success(let database): completion(database, nil)
}
}
}
}
Now we're ready to write a mock implementation of API. Based on the code you posted, it would look like this:
struct BadTestAPI: API {
let result: Result<Data, Error>
func fetch(completion: #escaping (Result<Data, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completion(result)
}
}
}
But I don't like this implementation for at least three reasons:
It hardcodes a 0.5 second delay. Yuck. We want test cases to run as fast as possible!
It doesn't make it easy to verify that fetch is only called once.
It doesn't let us control more precisely when it calls the completion handler.
Instead, let's write the mock implementation this way:
class TestAPI: API {
let ex = XCTestExpectation(description: "api.fetch called")
var completion: ((Result<Data, Error>) -> Void)? = nil
func fetch(completion: #escaping (Result<Data, Error>) -> Void) {
XCTAssertNil(self.completion)
self.completion = completion
ex.fulfill()
}
}
Now we can write the test case to use this implementation:
final class TestDatabase: XCTestCase {
func testGetDatabaseReturnsSameInstance() {
class Record {
let ex = XCTestExpectation()
var database: Database? = nil
}
let api = TestAPI()
let records = [Record(), Record(), Record()]
XCTAssertNil(api.completion)
for record in records {
Database.getDatabase(api: api) {
XCTAssertNil(record.database)
record.database = try! $0.get()
record.ex.fulfill()
}
}
self.wait(for: [api.ex], timeout: 10)
for record in records {
XCTAssertNil(record.database)
}
api.completion!(.success(Data()))
wait(for: records.map(\.ex), timeout: 10)
XCTAssertNotNil(records[0].database)
for record in records.dropFirst() {
XCTAssertEqual(record.database, records[0].database)
}
}
}
Here are some things this test case verifies:
api.fetch is not called before Database.getDatabase.
api.fetch is only called once.
No completion handler is called more than once.
The getDatabase completion handlers are called after the api.fetch completion handler.
Related
I tried to refactor a Firebase operation from the old completion handler to the new Task.init() and it seems that the operation is now taking longer. Am I doing something wrong? Are the await calls not being done concurrently (which is the reason I am calling both operations at the same time and counting how many finished with the completion handler approach)? Any suggestions for what might be causing the slower execution time?
Thank you in advance.
Completion handler approach (0.73s)
Previously I had this method:
extension Firebase {
static func getAll<T: Decodable>(_ subCollection: SubCollection,
completion: #escaping (_ result: [T]?, _ error: Error?) -> Void) {
db.collection("userData").document(currentUser!.uid).collection("\(subCollection)").getDocuments(completion: { (querySnapshot, error) in
var documents: [T] = []
if let error { print(error.localizedDescription) }
if let querySnapshot {
for document in querySnapshot.documents {
if let decodedDocument = try? document.data(as: T.self) { documents.append(decodedDocument) }
else { print("Failed to decode a retrieved document of type \(T.self) at getAll") }
}
}
completion(documents, error)
AppNotification.post(.firebseOperationCompleted)
})
}
}
Which I would use like this:
class Loading: UIViewController {
var error: Error?
var operationsCompleted = 0
let start = CFAbsoluteTimeGetCurrent()
private func fetchData() {
operationsCompleted = 0
Firebase.getAll(.goals, completion: { (results: [Goal]?, error) in
if let results { UltraDataStorage.goals = results }
if let error { self.error = error }
self.operationsCompleted += 1
})
Firebase.getAll(.ideas, completion: { (results: [Idea]?, error) in
if let results { UltraDataStorage.ideas = results }
if let error { self.error = error }
self.operationsCompleted += 1
})
#objc func advanceWhenOperationsComplete(){
print("Took \(CFAbsoluteTimeGetCurrent() - self.start) seconds")
if operationsCompleted == 2 {
// Proceed if error is nil
}
}
override func viewDidLoad() {
fetchData()
AppNotification.observe(handler: self, name: .firebseOperationCompleted, function: #selector(advanceWhenOperationsComplete))
}
}
Task.init() approach (1.14s)
Now, I updated the getAll function:
extension Firebase {
static func getAll<T: Decodable>(_ subCollection: SubCollection) async -> Result<[T], Error> {
do {
let documents = try await db.collection("userData").document(currentUser!.uid).collection("\(subCollection)").getDocuments()
var decodedDocuments: [T] = []
for document in documents.documents {
if let decodedDocument = try? document.data(as: T.self) { decodedDocuments.append(decodedDocument) }
else { print("Failed to decode a retrieved document of type \(T.self) at getAll") }
}
return.success(decodedDocuments)
}
catch { return.failure(error) }
}
}
And I am now calling it like this
class Loading: UIViewController {
var error: Error?
let start = CFAbsoluteTimeGetCurrent()
private func fetchData() {
Task.init(operation: {
let goalsResult: Result<[Goal], Error> = await Firebase.getAll(.goals)
switch goalsResult {
case .success(let goals): UltraDataStorage.goals = goals
case .failure(let error): self.error = error
}
let ideasResult: Result<[Idea], Error> = await Firebase.getAll(.ideas)
switch ideasResult {
case .success(let ideas): UltraDataStorage.ideas = ideas
case .failure(let error): self.error = error
}
DispatchQueue.main.async {
self.advanceWhenOperationsComplete()
}
})
}
func advanceWhenOperationsComplete(){
print("Took \(CFAbsoluteTimeGetCurrent() - self.start) seconds")
// Proceed when the async operations are completed
}
override func viewDidLoad() {
fetchData()
}
}
The performance difference is likely a result that the completion handler pattern is running the requests concurrently, but the async-await rendition is performing them sequentially. The latter is awaiting the result of the first asynchronous request before even initiating the next asynchronous request.
To get them to run concurrently, you can either use the async let pattern (see SE-0317) or use a task group:
extension Firebase {
static func getAll<T: Decodable>(_ subCollection: SubCollection) async throws -> [T] {
try await db
.collection("userData")
.document(currentUser!.uid)
.collection("\(subCollection)")
.getDocuments()
.documents
.map { try $0.data(as: T.self) }
}
}
// you could use `async let`
private func fetchData1() async throws {
async let goals: [Goal] = Firebase.getAll(.goals)
async let ideas: [Idea] = Firebase.getAll(.ideas)
UltraDataStorage.goals = try await goals
UltraDataStorage.ideas = try await ideas
advanceWhenOperationsComplete()
}
// or task group
private func fetchData2() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { UltraDataStorage.goals = try await Firebase.getAll(.goals) }
group.addTask { UltraDataStorage.ideas = try await Firebase.getAll(.ideas) }
try await group.waitForAll()
}
advanceWhenOperationsComplete()
}
(These might not be 100% right, as I do not implementations for all of these types and methods and therefore cannot compile this, but hopefully it illustrates the idea. Notably, I am nervous about the thread-safety of UltraDataStorage, especially in the task group example. But, that is beyond the scope of this question.)
Bottom line, async let is an intuitive way to run tasks concurrently and is most useful when dealing with a fixed, limited number of asynchronous tasks. Task groups shine when dealing with a variable number of tasks. That’s not the case here, but I include it for the sake of completeness.
Note, I’ve taken the liberty of excising the Result<[T], Error> type, and instead throw the error.
I'm trying to write some unit tests for my API using URLSession.DataTaskPublisher. I've found an already existing question on Stackoverflow for the same but I'm struggling to implement a working class using the proposed solution.
Here's the existing question: How to mock URLSession.DataTaskPublisher
protocol APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher
}
class APISessionDataTaskPublisher: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
return session.dataTaskPublisher(for: request)
}
var session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
}
class URLSessionMock: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
// How can I return a mocked URLSession.DataTaskPublisher here?
}
}
My API then uses the above like this:
class MyAPI {
/// Shared URL session
private let urlSession: APIDataTaskPublisher
init(urlSession: APIDataTaskPublisher = APISessionDataTaskPublisher(session: URLSession.shared)) {
self.urlSession = urlSession
}
}
What I don't know is how to implement URLSessionMock.dataTaskPublisher().
It would probably be simpler not to mock DataTaskPublisher. Do you really care if the publisher is a DataTaskPublisher? Probably not. What you probably care about is getting the same Output and Failure types as DataTaskPublisher. So change your API to only specify that:
protocol APIProvider {
typealias APIResponse = URLSession.DataTaskPublisher.Output
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError>
}
Conform URLSession to it for production use:
extension URLSession: APIProvider {
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
return dataTaskPublisher(for: request).eraseToAnyPublisher()
}
}
And then your mock can create the publisher in any way that's convenient. For example:
struct MockAPIProvider: APIProvider {
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!
let data = "Hello, world!".data(using: .utf8)!
return Just((data: data, response: response))
.setFailureType(to: URLError.self)
.eraseToAnyPublisher()
}
}
If you store in UT bundle stub JSON (XML, or something) for every API call that you want to test then the simplest mocking code might look as following
class URLSessionMock: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
// here might be created a map of API URLs to cached stub replies
let stubReply = request.url?.lastPathComponent ?? "stub_error"
return URLSession.shared.dataTaskPublisher(for: Bundle(for: type(of: self)).url(forResource: stubReply, withExtension: "json")!)
}
}
so instead call to network server your publisher is created with URL of locally stored resource with known data, so you can verify all your workflow.
I will develop the step from having a simple Get request, to mocking .dataTaskPublisher for Combine and for the last part, testing the call. It is a ready to use code, for everyone in case someone else would need it.
Follow the comment to add your model or anything that depends on your project data.
So this is the protocol that give the rules to my NetworkRequest class:
protocol NetworkRequestProtocol {
associatedtype Resource
var resourceURL: NetworkEndpoint { get set }
var resourceSession: URLSession { get set }
func download() -> AnyPublisher<Resource, NetworkError>
}
There is some custom class, NetworkEndpoint and NetworkError, you can add your own here if you want or use URL and URLError instead:
enum NetworkEndpoint {
static let baseURL = URL(string: "API_BASE_URL")! // Add your api base url here
case live
var url: URL {
switch self {
case .live:
return NetworkEndpoint.baseURL!.appendingPathComponent("END_OR_YOUR_API_URL") // Add the end of your API url here
}
}
}
enum NetworkError: LocalizedError {
case addressUnreachable(URL)
case invalidResponse
var errorDescription: String? {
switch self {
case .invalidResponse:
return "The server response is invalid."
case .addressUnreachable(let url):
return "\(url.absoluteString) is unreachable."
}
}
}
Now, I am creating the NetworkRequest class to handle the API call. RessourceSession initializer is used for the UnitTest part only:
final class NetworkRequest<Resource> where Resource: Codable {
var resourceURL: NetworkEndpoint
var resourceSession: URLSession
init(_ resourceURL: NetworkEndpoint,
resourceSession: URLSession = URLSession(configuration: .default)) {
self.resourceURL = resourceURL
self.resourceSession = resourceSession
}
// MARK: - Dispatch Queues
let downloadQueue = DispatchQueue(
label: "downloadQueue", qos: .userInitiated,
attributes: .concurrent, autoreleaseFrequency: .inherit, target: .main)
}
// MARK: - Network Requests
extension NetworkRequest: NetworkRequestProtocol {
func download() -> AnyPublisher<Resource, NetworkError> {
resourceSession
.dataTaskPublisher(for: resourceURL.url)
.receive(on: downloadQueue)
.map(\.data)
.decode(type: Resource.self, decoder: JSONDecoder())
.mapError { error -> NetworkError in
switch error {
case is URLError:
return .addressUnreachable(self.resourceURL.url)
default:
return .invalidResponse }}
.eraseToAnyPublisher()
}
}
For the production code, this is an example of use of the NetworkRequest class, and of course, your model must be Codable:
var subscriptions = Set<AnyCancellable>()
func downloadData() {
NetworkRequest<YOUR_MODEL_NAME>(.live).download() // Add your model name inside the brackets
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error)
case .finished:
break }},
receiveValue: { data in
print(data) })
.store(in: &subscriptions)
}
So now that all the code is setup in the project, we can pass to the UnitTest part and start mocking URLSession:
class MockURLSession: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
init(data: Data?, response: URLResponse?, error: Error?) {
self.data = data
self.response = response
self.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 MockURLSessionDataTask {
completionHandler(data, response, error)
}
}
}
Now, we mock URLSessionDataTask that we return when overriding dataTask in MockURLSession, and it will work for .dataTaskPublisher:
class MockURLSessionDataTask: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
We create fake response data to pass into our tests, but you must create a .json file with your data in it to fetch them in the tests:
class FakeResponseData {
static let response200OK = HTTPURLResponse(url: URL(string: "https://test.com")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)!
static let responseKO = HTTPURLResponse(url: URL(string: "https://test.com")!,
statusCode: 500,
httpVersion: nil,
headerFields: nil)!
class RessourceError: Error {}
static let error = RessourceError()
static var correctData: Data {
let bundle = Bundle(for: FakeResponseData.self)
let fakeJsonURL = bundle.url(forResource: "FAKE_JSON_FILE_NAME", withExtension: "json") // Add your fake json file name in here
let fakeJsonData = try! Data(contentsOf: fakeJsonURL!)
return fakeJsonData
}
static let incorrectData = "error".data(using: .utf8)!
}
And to finish, this is the part where you test your NetworkRequest, with the fake data coming from the .json file, or the error. You use resourceSession initializer to add your MockURLSession here and avoid making real network call:
class NetworkRequestTests: XCTestCase {
var expectation: XCTestExpectation!
var subscriptions: Set<AnyCancellable>!
override func setUpWithError() throws {
try super.setUpWithError()
expectation = XCTestExpectation(description: "wait for queue change")
subscriptions = Set<AnyCancellable>()
}
override func tearDownWithError() throws {
subscriptions = nil
expectation = nil
try super.tearDownWithError()
}
func testNetworkRequest_mockURLSessionAddCorrectDataResponse_returnRatesDataModelValues() throws {
let expectedTestValue = "test" // This value is set in your .json fake data for testing
// This is where you use resourceSession to pass your fake data
let networkRequest = NetworkRequest<RatesData>(.live, resourceSession:
MockURLSession(data: FakeResponseData.correctData,
response: FakeResponseData.response200OK,
error: nil))
networkRequest.download()
.sink(
receiveCompletion: { completion in
self.expectation.fulfill() },
receiveValue: { value in
XCTAssertEqual(expectedTimestamp, value.InFakeJson) // Compare with your fake json file
})
.store(in: &subscriptions)
wait(for: [expectation], timeout: 0.1)
}
func testNetworkRequest_mockURLSessionAddServerErrorAsResponse_returnNetworkErrorInvalidResponse() throws {
let expectedNetworkError = NetworkError.invalidResponse.localizedDescription
// This is where you use resourceSession to pass your fake data
let networkRequest = NetworkRequest<RatesData>(.live, resourceSession:
MockURLSession(data: nil,
response: FakeResponseData.responseKO,
error: nil))
networkRequest.download()
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTAssertEqual(expectedNetworkError, error.localizedDescription)
case .finished:
break
}
self.expectation.fulfill() },
receiveValue: { value in
XCTAssertNil(value)
})
.store(in: &subscriptions)
wait(for: [expectation], timeout: 0.1)
}
}
Answered on original question, but will repost here:
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).
Using DispatchGroup I am trying to run 2 network requests against my client, returning the results when both have completed.
I am having an issue in that sometimes the completion handler for one of DispatchGroup requests is called twice and the other is not called at all.
An example would be -
func fetchProfileWithRelatedArticle(onSuccess: #escaping (User, [RelatedArticle]) -> Void, onError: #escaping (Error) -> Void) {
let dispatchGroup = DispatchGroup()
var user: User?
var articles: [RelatedArticle] = []
var errors: [Error] = []
dispatchGroup.enter()
fetchProfileForUser(onSuccess: {
user = $0
print("fetchProfile:",$0)
print("123")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.enter()
getArticlesForUser(onSuccess: {
articles = $0
print("getArticlesForUser:",$0)
print("456")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .main) {
guard let user = user, errors.isEmpty else { return }
onSuccess(user, articles)
}
}
Here I fetch a user profile and also fetch a list of articles they have written. These are returned via a completion handler and presented elsewhere.
Most of the time this works, however it appears on occasion either one of those requests will call its own completion handler twice and the other request wont.
I suspect this may be down to when my access token expires as it occurs if I leave the app for a short time. My access token has a life of 2 minutes.
Should a request receive a 401 response, I have the following method in my network client that requests a new token, then invokes the call again. I believe this may not be working as I'd like.
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
I suspect calling the method again after the update is doing something to the requests my dispatch group is returning.
Is it possible to chain requests in this fashion?
struct NoContent: Codable { }
typealias RefreshHandler = (#escaping (Bool) -> Void) -> ()
typealias TokenGetter = () -> [String: String]
protocol ClientType: class {
associatedtype Route: RouterType
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void)
}
class Client<Route: RouterType>: ClientType {
enum APIError: Error {
case unknown, badResponse, jsonDecoder, other
}
enum TokenError: String, Error {
case expired = "Access Token Expired"
case refused = "Refresh Token Failed"
}
private(set) var session: SessionType
private(set) var tokenGetter: TokenGetter
private(set) var refreshHandler: RefreshHandler
private lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
return decoder
}()
init(session: SessionType, tokenGetter: #escaping TokenGetter, refreshHandler: #escaping RefreshHandler) {
self.session = session
self.tokenGetter = tokenGetter
self.refreshHandler = refreshHandler
}
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void) {
let request = URLRequest(
resource: resource,
headers: tokenGetter()
)
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard error == nil else { completion(.failure(APIError.unknown)); return }
guard let response = response as? HTTPURLResponse else { completion(.failure(APIError.badResponse)); return }
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
if response.statusIsSuccess() {
guard let self = self, let data = self.deserializeNoContentResponse(data: data) else { completion(.failure(APIError.badResponse)); return }
do {
let value = try self.decoder.decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(value))
}
} catch let error {
print(error)
}
return
}
completion(.failure(APIError.other))
}.resume()
}
// some calls return a 200/201 with no data
private func deserializeNoContentResponse(data: Data?) -> Data? {
if data?.count == 0 {
return "{ }".data(using: .utf8)
}
return data
}
}
Sounds like you would need to something like the following in your networking client:
func makeTheRequest(_ completion: CompletionHandler) {
URLSession.shared.dataTask(with: someURL) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else {
return
}
if httpResponse.statusCode == 401 {
self.refreshToken { success in
if success { self.makeTheRequest(completion) }
}
}
// handle response
completion(whateverDataYouNeedToPass)
}
}
That would make the call, check the response code, refresh the token is needed and if that succeeds it calls the method that should make the response again with the completion handler that was passed to in the first place without calling it first. So the completion handler wouldn't be called until after the API call is made the second time.
Of course, adopt this for your own code, shouldn't be too hard to do
I tried to use a generic Json Decoder for all of my models using a protrocol.
//Here the definition of the protocol:
func fetch<T: Decodable>(with request: URLRequest, decode: #escaping (Decodable) -> T?, completion: #escaping (Result<T, APIError>) -> Void) {.. other Code}
//Here the implementation:
func getData(from endPoint: Endpoint, completion: #escaping (Result<ApiResponseArray<Codable>, APIError>) -> Void) {
let request = endPoint.request
fetch(with: request, decode: { json -> Decodable in
guard let dataResult = json as? modelData else { return nil }
return dataResult
}, completion: completion)
}
ApiResponseArray gives me the error: Protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Decodable' because only concrete types can conform to protocols. But how can I implement a generic decoder and passing them different models. I think I have to modify my protocol definition but how? I would like to pass the model and then receive the decoded data for the model (in my example modelData). Its obvious that the program runs when I write:
func getData(from endPoint: Endpoint, completion: #escaping (Result, APIError>) I mean when I use the concrete Model, but I want to pass the model, so that I can use the class for different models.
Thanks,
Arnold
A protocol cannot conform to itself, Codable must be a concrete type or can only be used as a generic constraint.
In your context you have to do the latter, something like this
func fetch<T: Decodable>(with request: URLRequest, decode: #escaping (Data) throws -> T, completion: #escaping (Result<T, APIError>) -> Void) { }
func getData<T: Decodable>(_ : T.Type = T.self, from endPoint: Endpoint, completion: #escaping (Result<T, APIError>) -> Void) {
let request = endPoint.request
fetch(with: request, decode: { data -> T in
return try JSONDecoder().decode(T.self, from: data)
}, completion: completion)
}
A network request usually returns Data which is more reasonable as parameter type of the decode closure
I can suggest to you how to use Decodable with your API call structure by using Alamofire.
I have created RequestManager class which inherits from SessionManager and added request call inside which common to all.
class RequestManager: SessionManager {
// Create shared instance
static let shared = RequestManager()
// Create http headers
lazy var httpHeaders : HTTPHeaders = {
var httpHeader = HTTPHeaders()
httpHeader["Content-Type"] = "application/json"
httpHeader["Accept"] = "application/json"
return httpHeader
}()
//------------------------------------------------------------------------------
// MARK:-
// MARK:- Request Methods
//------------------------------------------------------------------------------
func responseRequest(_ url: String, method: Alamofire.HTTPMethod, parameter: Parameters? = nil, encoding: ParameterEncoding, header: HTTPHeaders? = nil, completionHandler: #escaping (DefaultDataResponse) -> Void) -> Void {
self.request(url, method: method, parameters: parameter, encoding: encoding, headers: header).response { response in
completionHandler(response)
}
}
}
Then after one more class created NetworkManager class which hold required get/post method call and decode json by JSONDecoder as follow:
class NetworkManager {
static let shared = NetworkManager()
var progressVC : ProgressVC?
//----------------------------------------------------------------
// MARK:-
// MARK:- Get Request Method
//----------------------------------------------------------------
func getResponse<T: Decodable>(_ url: String, parameter: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, header: HTTPHeaders? = nil, showHUD: HUDFlag = .show, message: String? = "Please wait...", decodingType: T.Type, completion: #escaping (Decodable?, APIError?) -> Void) {
DispatchQueue.main.async {
self.showHideHud(showHUD: showHUD, message: "")
}
RequestManager.shared.responseRequest(url, method: .get, parameter: parameter, encoding: encoding, header: header) { response in
DispatchQueue.main.async {
self.showHideHud(showHUD: .hide, message: "")
}
guard let httpResponse = response.response else {
completion(nil, .requestFailed("Request Failed"))
return
}
if httpResponse.statusCode == 200 {
if let data = response.data {
do {
let genericModel = try JSONDecoder().decode(decodingType, from: data)
completion(genericModel, nil)
} catch {
do {
let error = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
if let message = error!["message"] as? String {
completion(nil, .errorMessage(message)!)
} else if let message = error!["message"] as? Int {
completion(nil, .errorMessage(String(describing: "Bad Request = \(message)")))
}
} catch {
completion(nil, .jsonConversionFailure("JSON Conversion Failure"))
}
}
} else {
completion(nil, .invalidData("Invalid Data"))
}
} else {
completion(nil, .responseUnsuccessful("Response Unsuccessful"))
}
}
}
}
ProgressVC is my custom class to show progress view when api call.
After that, I have created DataManager class which will help me to create request url.
class DataManager: NSObject {
//------------------------------------------------------------------------------
// MARK:- Variables
//------------------------------------------------------------------------------
static let shared = DataManager()
let baseUrl = WebServiceURL.local
//------------------------------------------------------------------------------
// MARK:- Custom Methods
//------------------------------------------------------------------------------
// Get API url with endpoints
func getURL(_ endpoint: WSEndPoints) -> String {
return baseUrl + endpoint.rawValue
}
}
I have created following enum to send data or error in my completion block.
enum Result<T, U> where U: Error {
case success(T)
case failure(U)
}
Here is list of error which stored custom message related to status fired during api call.
enum APIError: Error {
case errorMessage(String)
case requestFailed(String)
case jsonConversionFailure(String)
case invalidData(String)
case responseUnsuccessful(String)
case jsonParsingFailure(String)
var localizedDescription: String {
switch self {
case.errorMessage(let msg):
return msg
case .requestFailed(let msg):
return msg
case .jsonConversionFailure(let msg):
return msg
case .invalidData(let msg):
return msg
case .responseUnsuccessful(let msg):
return msg
case .jsonParsingFailure(let msg):
return msg
}
}
}
Then after, I will extend this DataManager class to call web service based on module. So I will create Swift file and extend DataManager class and call relative API.
See following, In API call I will return relative model into Result like Result<StoreListModel, APIError>
extension DataManager {
// MARK:- Store List
func getStoreList(completion: #escaping (Result<StoreListModel, APIError>) -> Void) {
NetworkManager.shared.getResponse(getURL(.storeList), parameter: nil, encoding: JSONEncoding.default, header: getHeaders("bd_suvlascentralpos"), showHUD: .show, message: "Please wait...", decodingType: StoreListModel.self) { (decodableData, apiError) in
if apiError != nil {
completion(.failure(apiError!))
} else {
guard let userData = decodableData as? StoreListModel else {
completion(.failure(apiError!))
return
}
completion(.success(userData))
}
}
}
}
From completion block of request I will get decodable data which here safely type cast.
Use:
DataManager.shared.getStoreList { (result) in
switch result {
case .success(let storeListModel):
if let storeList = storeListModel, storeList.count > 0 {
self.arrStoreList = storeList
self.tblStoreList.isHidden = false
self.labelEmptyData.isHidden = true
self.tblStoreList.reloadData()
} else {
self.tblStoreList.isHidden = true
self.labelEmptyData.isHidden = false
}
break
case .failure(let error):
print(error.localizedDescription)
break
}
}
Note:- Some variables, models classes are my custom. You can replace it with your.
I have an array of dispatch workItems, how to wait until one work is completed before i move on to the next work in the queue?
func AsyncCalls(statusHandler: #escaping (String) -> Void){
var dispatchWorkItems : [DispatchWorkItem] = []
let categoryWorkItem = DispatchWorkItem {
main {
return statusHandler("Loading categories ")
}
self.modelView.getCategories(completion: { data,error in
main {
if data.isEmpty {
return statusHandler("\(error )")
}else{
return statusHandler("Done loading categories")
}
}
})
}
let itemsWorkItem = DispatchWorkItem {
main {
return statusHandler("Loading Inventory ")
}
self.modelView.getInventory(completion: { data,error in
main {
if data.isEmpty {
return statusHandler("\(error )")
}else{
return statusHandler("Done loading Inventory")
}
}
})
}
dispatchWorkItems.append(categoryWorkItem)
dispatchWorkItems.append(itemsWorkItem)
let queue = DispatchQueue(label: "com.dataLoader")
let group = DispatchGroup()
dispatchWorkItems.forEach{queue.async(group: group, execute: $0)}
group.notify(queue: .main) {
main{
}
}
}
How can i simplify the above method or how do i apply semaphores or any other accepted approach to help me wait until i get a response from a DispatchworkItem before moving on to execute the next DispatchworkItem in a queue
A modelView to getData from server looks something like the below
func getInventory(completion: #escaping ArrayClosure<[InventoryClass], String>){
let parameters : [(String,AnyObject)] = [
("PageNumber" , "1" as AnyObject),
("Limit","1000" as AnyObject),
("BranchIds","\(business.branch?.id ?? "")" as AnyObject),
("canBeSold","true" as AnyObject)
]
InventoryService(authorizationHeader: self.header).getInventory(parameters: parameters) { request in
switch request {
case .success(let data):
guard let finalData = data.data else {return completion([], "Request to get Inventory Items was sucessfull but items count is 0")}
return completion([finalData],"")
case .failure(let error):
return completion([],error.localizedDescription)
}
}
}
I might advise against using semaphores or the like to block threads so that you can make asynchronous tasks behave synchronously, solely for the sake of DispatchWorkItem.
When I want to establish dependencies between asynchronous tasks, I have historically used Operation rather than DispatchWorkItem. (Admittedly, in iOS 13 and later, we might contemplate Combine’s Future/Promise, but for now operations are the way to go.) Operations have been designed to support wrapping of asynchronous processes much more elegantly than DispatchWorkItem. So you can use a queue whose maxConcurrentOperationCount is 1, like so:
let networkQueue = OperationQueue()
networkQueue.maxConcurrentOperationCount = 1
let completionOperation = BlockOperation {
print("all done")
}
for url in urls {
let operation = NetworkOperation(url: url) { result in
switch result {
case .failure(let error):
...
case .success(let data):
...
}
}
completionOperation.addDependency(operation)
networkQueue.addOperation(operation)
}
OperationQueue.main.addOperation(completionOperation)
Or you can use a more reasonable maxConcurrentOperationCount and use dependencies only between those operations where you need this sequential behavior:
let networkQueue = OperationQueue()
networkQueue.maxConcurrentOperationCount = 4
let completionOperation = BlockOperation {
print("all done")
}
var previousOperation: Operation?
for url in urls {
let operation = NetworkOperation(url: url) { result in
switch result {
case .failure(let error):
...
case .success(let data):
...
}
}
if let previousOperation = previousOperation {
operation.addDependency(previousOperation)
}
completionOperation.addDependency(operation)
networkQueue.addOperation(operation)
previousOperation = operation
}
OperationQueue.main.addOperation(completionOperation)
This is what that NetworkOperation might look like:
class NetworkOperation: AsynchronousOperation {
typealias NetworkCompletion = (Result<Data, Error>) -> Void
enum NetworkError: Error {
case invalidResponse(Data, URLResponse?)
}
private var networkCompletion: NetworkCompletion?
private var task: URLSessionTask!
init(request: URLRequest, completion: #escaping NetworkCompletion) {
super.init()
task = URLSession.shared.dataTask(with: request) { data, response, error in
defer {
self.networkCompletion = nil
self.finish()
}
guard let data = data, error == nil else {
self.networkCompletion?(.failure(error!))
return
}
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
self.networkCompletion?(.failure(NetworkError.invalidResponse(data, response)))
return
}
self.networkCompletion?(.success(data))
}
networkCompletion = completion
}
convenience init(url: URL, completion: #escaping NetworkCompletion) {
self.init(request: URLRequest(url: url), completion: completion)
}
override func main() {
task.resume()
}
override func cancel() {
task.cancel()
}
}
This is passing back Data, but you can write permutations/subclasses that further parse that into whatever your web service is returning using JSONDecoder or whatever. But hopefully this illustrates the basic idea.
The above uses this AsynchronousOperation class:
/// Asynchronous operation base class
///
/// This is abstract to class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `Operation` subclass. You can subclass this and
/// implement asynchronous operations. All you must do is:
///
/// - override `main()` with the tasks that initiate the asynchronous task;
///
/// - call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `finish()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `finish()` is called.
public class AsynchronousOperation: Operation {
/// State for this operation.
#objc private enum OperationState: Int {
case ready
case executing
case finished
}
/// Concurrent queue for synchronizing access to `state`.
private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)
/// Private backing stored property for `state`.
private var _state: OperationState = .ready
/// The state of the operation
#objc private dynamic var state: OperationState {
get { stateQueue.sync { _state } }
set { stateQueue.sync(flags: .barrier) { _state = newValue } }
}
// MARK: - Various `Operation` properties
open override var isReady: Bool { return state == .ready && super.isReady }
public final override var isAsynchronous: Bool { return true }
public final override var isExecuting: Bool { return state == .executing }
public final override var isFinished: Bool { return state == .finished }
// KVN for dependent properties
open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
if ["isReady", "isFinished", "isExecuting"].contains(key) {
return [#keyPath(state)]
}
return super.keyPathsForValuesAffectingValue(forKey: key)
}
// Start
public final override func start() {
if isCancelled {
state = .finished
return
}
state = .executing
main()
}
/// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception.
open override func main() {
fatalError("Subclasses must implement `main`.")
}
/// Call this function to finish an operation that is currently executing
public final func finish() {
if isExecuting { state = .finished }
}
}
There are lots of ways to write a base AsynchronousOperation, and I don’t want to get lost in the details, but the idea is that we now have an Operation that we can use for any asynchronous process.