How would you test this generic API? - swift

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
}

Related

Mock Method in Swift

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.

How can I return all the response from API to my Swift app

I'am learning swift and I see an example here https://matteomanferdini.com/network-requests-rest-apis-ios-swift/ and Im trying to change the code for something that work for me.
this is how the original code looks
struct Wrapper<T: Decodable>: Decodable {
let items: [T]?
}
protocol NetworkRequest: AnyObject {
associatedtype ModelType
func decode(_ data: Data) -> ModelType?
func load(withCompletion completion: #escaping (ModelType?) -> Void)
}
extension NetworkRequest {
fileprivate func load(_ url: URLRequest, withCompletion completion: #escaping (ModelType?) -> Void) {
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
let task = session.dataTask(with: url, completionHandler: { [weak self] (data: Data?, response: URLResponse?, error: Error?) -> Void in
if let error = error {
print("Error: \(error)")
}
guard let data = data else {
completion(nil)
return
}
completion(self?.decode(data))
})
task.resume()
}
}
class APIRequest<Resource: APIResource> {
let resource: Resource
init(resource: Resource) {
self.resource = resource
}
}
extension APIRequest: NetworkRequest {
func decode(_ data: Data) -> [Resource.ModelType]? {
let wrapper = try? JSONDecoder().decode(Wrapper<Resource.ModelType>.self, from: data)
return wrapper?.items
}
func load(withCompletion completion: #escaping ([Resource.ModelType]?) -> Void) {
load(resource.request, withCompletion: completion)
}
}
but what I need to change the structure Wrapper to
struct Wrapper<T: Decodable>: Decodable {
let items: [T]?
let response: Bool?
let message: String?
}
and return items, response and message not only items
In this case you don't need the protocol at all because you want to get the root object.
This is sufficient
struct Wrapper<T: Decodable>: Decodable {
let items: [T]
let response: Bool
let message: String
}
class NetworkRequest {
func load<T : Decodable>(_ request: URLRequest, withCompletion completion: #escaping (Result<Wrapper<T>,Error>) -> Void) {
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
let task = session.dataTask(with: request) { data, _, error in
if let error = error {
completion(.failure(error))
} else {
completion( Result {try JSONDecoder().decode(Wrapper<T>.self, from: data!)})
}
}
task.resume()
}
}
The completion handler returns a Result object, on success the wrapper object and on failure all errors.
In the wrapper struct declare all properties non-optional to get error messages and change only those to optional which really can be nil.
I change the code like this
class NetworkRequest<Resource: APIResource> {
let resource: Resource
init(resource: Resource) {
self.resource = resource
}
func load(withCompletion completion: #escaping (Result<Wrapper<Resource.ModelType>,Error>) -> Void) {
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
let task = session.dataTask(with: self.resource.request) { data, _, error in
if let error = error {
completion(.failure(error))
} else {
completion( Result {try JSONDecoder().decode(Wrapper<Resource.ModelType>.self, from: data!)})
}
}
task.resume()
}
}
struct LoginResource: APIResource {
typealias ModelType = Token
let methodPath = "/users/login/"
let method = "post"
var params: [String: Any]?
init(username: String, password: String) {
self.params = ["username":username, "password": password]
}
}
In my view:
func login() {
if user == "" || password == "" {
self.title_alert = "Info"
message_alert = "Test Alert"
show_alert = true
return
}
let loginRequest = NetworkRequest(resource: LoginResource(username:user,password:password))
loginRequest.load { result in
switch result {
case .failure(let error):
print(error)
case .success(let data):
print(data)
}
}
}
I don't know if this is the best way but works Thank you #vadian

How to mock DataTaskPublisher?

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

Generic Decoder for Swift using a protocol

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.

Change response model / shape depending on path

I have a networking layer I am using to call multiple endpoints. I'd like to reduce the amount of repeated code and thought perhaps I could pass my response model as part of my endpoint.
The idea would be instead of needing multiple functions that simply differ by response, I could just call my network layer and have this set based on the path.
The current error I see is
Var 'responseType' is not a member type of 'IdentityEndpoint'
I was hoping to achieve something like this
mutating func identity(with endpoint: IdentityEndpoint, completion: #escaping (Either<IdentityEndpoint.responseType>) -> Void)
instead of this
mutating func identity(with endpoint: IdentityEndpoint, completion: #escaping (Either<OAuthToken>) -> Void)
APIClient
struct APIClient: APIClientProtocol {
var task: URLSessionDataTask = URLSessionDataTask()
var session: SessionProtocol = URLSession.shared
var request: URLRequest?
mutating func identity(with endpoint: IdentityEndpoint, completion: #escaping (Either<IdentityEndpoint.responseType>) -> Void) {
dispatch(endpoint: endpoint, completion: completion)
}
}
extension APIClient {
fileprivate mutating func dispatch<T: Codable>(endpoint: EndpointProtocol, completion: #escaping (Either<T>) -> Void) {
do {
request = try constructRequest(from: endpoint)
guard let request = request else { return }
call(with: request, completion: completion)
} catch {}
}
fileprivate func constructRequest(from route: EndpointProtocol) throws -> URLRequest {
var request = URLRequest(url: route.baseUrl.appendingPathComponent(route.path), cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 10.0)
request.httpMethod = route.httpMethod.rawValue
do {
switch route.task {
case .request(let headers):
addAdditionalHeaders(headers, request: &request)
case .requestParams(let bodyParams, let encoding, let urlParams, let headers):
addAdditionalHeaders(headers, request: &request)
try configureParameters(bodyParams: bodyParams, encoding: encoding, urlParams: urlParams, request: &request)
}
return request
} catch {
throw NSError(domain: "Could not create request task for \(route.task)", code: 0, userInfo: nil)
}
}
fileprivate func configureParameters(bodyParams: Parameters?, encoding: ParameterEncoding, urlParams: Parameters?, request: inout URLRequest) throws {
do {
try encoding.encode(urlRequest: &request, bodyParams: bodyParams, urlParams: urlParams)
} catch {
throw NSError(domain: "Could not configure params for request", code: 0, userInfo: nil)
}
}
fileprivate func addAdditionalHeaders(_ additionalHeaders: HTTPHeaders?, request: inout URLRequest) {
guard let headers = additionalHeaders else { return }
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}
}
IdentityEndPoint
protocol EndpointProtocol {
var baseUrl: URL { get }
var path: String { get }
var httpMethod: HTTPMethod { get }
var task: HTTPTask { get }
var headers: HTTPHeaders? { get }
}
public enum IdentityEndpoint {
case accessToken(company: String, code: String)
func getDomain(forService service: String) -> URL {
return URL(string: "https://{SERVICE}.foo.bar".replacingOccurrences(of: "{SERVICE}", with: service))!
}
}
extension IdentityEndpoint: EndpointProtocol {
var baseUrl: URL {
return getDomain(forService: "identity")
}
var responseType: Codable {
switch self {
default:
return OAuthToken.self as! Codable
}
}
var path: String {
switch self {
case .accessToken(let props):
return "/auth/realms/\(props.company)/protocol/openid-connect/token"
}
}
var httpMethod: HTTPMethod {
switch self {
case .accessToken:
return .POST
}
}
var headers: HTTPHeaders? {
switch self {
case .accessToken:
return ["Content-Type": "application/x-www-form-urlencoded"]
}
}
var task: HTTPTask {
switch self {
case .accessToken(let props):
return .requestParams(bodyParams: [
"grant_type": "authorization_code", "code": "\(props.code)", "redirect_uri": "homedev://oauth-callback", "client_id": "mobile-home"
], encoding: .jsonEncoding, urlParams: nil, headers: headers)
}
}
}
Add an associatedtype to your EndpointProtocol. Then specify it in IdentityEndpoint like this
protocol EndpointProtocol {
associatedtype ResponseType
...
}
extension IdentityEndpoint: EndpointProtocol {
typealias ResponseType = OAuthToken
...
}
Now you will be able to write
mutating func identity(
with endpoint: IdentityEndpoint,
completion: #escaping (Either<IdentityEndpoint.ResponseType>) -> Void
)