Write unit test for function that uses URLSession and RxSwift - swift

I have a function that creates and returns Observable that downloads and decodes data using URLSession. I wanted to write unit test for this function but have no idea how to tackle it.
function:
func getRecipes(query: String, _ needsMoreData: Bool) -> Observable<[Recipes]> {
guard let url = URL(string: "https://api.spoonacular.com/recipes/search?\(query)&apiKey=myApiKey") else {
return Observable.just([])
}
return Observable.create { observer in
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
return
}
do {
if self.recipes == nil {
self.recipes = try self.decoder.decode(Recipes.self, from: data)
self.dataList = self.recipes.results
self.baseUrl = self.recipes.baseUrl
} else {
if needsMoreData {
self.recipes = try self.decoder.decode(Recipes.self, from: data)
self.dataList.append(contentsOf: self.recipes.results.suffix(50))
} else {
self.dataList = try self.decoder.decode(Recipes.self, from: data).results
}
}
observer.onCompleted()
} catch let error {
observer.onError(error)
}
}
task.resume()
return Disposables.create {
task.cancel()
}
}
.trackActivity(activityIndicator)
}

The obvious answer is to inject the dataTask instead of using the singleton inside your function. Something like this:
func getRecipes(query: String, _ needsMoreData: Bool, dataTask: #escaping (URL, #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask) -> Observable<[Recipes]> {
guard let url = URL(string: "https://api.spoonacular.com/recipes/search?\(query)&apiKey=myApiKey") else {
return Observable.just([])
}
return Observable.create { observer in
let task = dataTask(url) { (data, response, error) in
// and so on...
You would call it in the main code like this:
getRecipes(query: "", false, dataTask: URLSession.shared.dataTask(with:completionHandler:))
In your test, you would need something like this:
func fakeDataTask(_ url: URL, _ completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
XCTAssertEqual(url, expectedURL)
completionHandler(testData, nil, nil)
return URLSessionDataTask()
}
let result = getRecipes(query: "", false, dataTask: fakeDataTask)
Did you know that URLSession has Reactive extensions already created for it? The one I like best is: URLSession.shared.rx.data(request:) which returns an Observable which will emit an error if there are any problems getting the data. I suggest you use it.

Related

Call function with completion handler

I created the function below but I'm not sure how to call it, it complains saying:
Type 'T.Type' cannot conform to 'Decodable'
Here's how I'd like to call it:
let result = getApiData(modelToDecode: MyModel, url: "abc")
This is what I've tried:
func getApiData<T : Decodable>(modelToDecode: T.Type, url: String) -> Any? {
// I get an error below
fetchDataAndDecode(url: String, modelToDecode: T.Type) { result in
}
// temp placeholder
return nil
}
func fetchDataAndDecode<T : Decodable>(url: String, modelToDecode: T.Type, completionHandler: #escaping (Result<T.Type, NetworkError>) -> Void) {
guard let url = URL(string: url) else {
completionHandler(.failure(NetworkError.badURL))
return
}
AF.request(url, method: .get).validate().responseData { response in
guard let data = response.data else {
completionHandler(.failure(NetworkError.apiFailed))
return
}
do {
// Decode the data
let decodedData = try JSONDecoder().decode(modelToDecode.self, from: data)
DispatchQueue.main.async {
completionHandler(.success(decodedData as! T.Type))
}
} catch(let error) {
print("🛑 Error on afRequest(): \(error)")
}
}
}
How can I call it inside the class properly?
Result should be Result<T, NetworkError>. No need to add modelToDecode to your method declaration. You can explicitly set the resulting type in your async call. Btw you should the completion handler as well if you fail to decode your data:
enum NetworkError: Error {
case badURL, apiFailed, corruptedData
}
Your method should look like this:
func fetchDataAndDecode<T: Decodable>(url: String, completionHandler: #escaping (Result<T, NetworkError>) -> Void) {
guard let url = URL(string: url) else {
completionHandler(.failure(.badURL))
return
}
AF.request(url, method: .get).validate().responseData { response in
guard let data = response.data else {
completionHandler(.failure(.apiFailed))
return
}
do {
let decodedData = try JSONDecoder().decode(T.self, from: data)
DispatchQueue.main.async {
completionHandler(.success(decodedData))
}
} catch {
completionHandler(.failure(.corruptedData))
}
}
}
And when calling it you need to explicitly set the resulting type:
fetchDataAndDecode(url: "yourURL") { (result: Result<WhatEver, NetworkError>) in
// switch the result here
}

Swift generics in completion handler

Im trying to refactor dat fetching func to enable it for several Decodable struct types.
func fetchData<T: Decodable>(_ fetchRequest: FetchRequestType, completion: #escaping ((Result<T, Error>) -> Void)) {
guard !isFetching else { return }
isFetching = true
guard let url = getURL(fetchRequest) else { assertionFailure("Could not compose URL"); return }
let urlRequest = URLRequest(url: url)
self.session.dataTask(with: urlRequest) { [unowned self] (data, response, error) in
guard let response = response as? HTTPURLResponse,
response.statusCode == 200 else {
self.isFetching = false
completion(.failure(NSError()))
return
}
guard let data = data else { assertionFailure("No data"); return }
if let jsonData = try? JSONDecoder().decode(T.self, from: data) {
self.isFetching = false
completion(.success(jsonData))
} else {
assertionFailure("Could not decode JSON data"); return
}
}.resume()
}
But when Im calling the func from controller with one of Decodable types I get a compile error
Generic parameter 'T' could not be inferred
networkClient.fetchData(.accountsSearch(searchLogin: text, pageNumber: 1)) { [unowned self] result in
switch result {
case .success(let dataJSON):
let accountsListJSON = dataJSON as! AccountsListJSON
let fetchedAccounts = accountsListJSON.items
.map({ AccountGeneral(login: $0.login, id: $0.id, avatarURLString: $0.avatarURL, type: $0.type) })
self.accounts = fetchedAccounts
case .failure(_):
assertionFailure("Fetching error!")
}
}
Please help me to find out what happened and solve a problem.
You can generally help the compiler to infer the T type by providing the result type, when you call fetchData(_:completion:) function like this:
networkClient.fetchData(
.accountsSearch(searchLogin: text, pageNumber: 1)
) { [unowned self] (result: Result<AccountsListJSON, Error>) in
...
}
If the method doesn't have a return type where the static type can be specified you have to add a parameter
func fetchData<T: Decodable>(_ fetchRequest: FetchRequestType, type: T.Type, completion: #escaping (Result<T, Error>) -> Void) { ...
and call it
networkClient.fetchData(.accountsSearch(searchLogin: text, pageNumber: 1), type: AccountsListJSON.self) { [unowned self] result in
and delete the downcast as! AccountsListJSON

Execute func after first func

self.werteEintragen() should start after weatherManager.linkZusammenfuegen() is done. Right now I use DispatchQueue and let it wait two seconds. I cannot get it done with completion func because I dont know where to put the completion function.
This is my first Swift file:
struct DatenHolen {
let fussballUrl = "deleted="
func linkZusammenfuegen () {
let urlString = fussballUrl + String(Bundesliga1.number)
perfromRequest(urlString: urlString)
}
func perfromRequest(urlString: String)
{
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
if error != nil{
print(error!)
return
}
if let safeFile = gettingInfo {
self.parseJSON(datenEintragen: safeFile)
}
}
task.resume()
}
}
func parseJSON(datenEintragen: Data) {
let decoder = JSONDecoder()
do {
let decodedFile = try decoder.decode(JsonDaten.self, from: datenEintragen)
TeamOne = decodedFile.data[0].home_name
} catch {
print(error)
}
}
}
And this is my second Swift File as Viewcontroller.
class HauptBildschirm: UIViewController {
func werteEintragen() {
Tone.text = TeamOne
}
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.linkZusammenfuegen()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [unowned self] in
self.werteEintragen()
}
}
}
How can I implement this and where?
func firstTask(completion: (_ success: Bool) -> Void) {
// Do something
// Call completion, when finished, success or faliure
completion(true)
}
firstTask { (success) in
if success {
// do second task if success
secondTask()
}
}
You can have a completion handler which will notify when a function finishes, also you could pass any value through it. In your case, you need to know when a function finishes successfully.
Here is how you can do it:
func linkZusammenfuegen (completion: #escaping (_ successful: Bool) -> ()) {
let urlString = fussballUrl + String(Bundesliga1.number)
perfromRequest(urlString: urlString, completion: completion)
}
func perfromRequest(urlString: String, completion: #escaping (_ successful: Bool) -> ()) {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
guard error == nil else {
print("Error: ", error!)
completion(false)
return
}
guard let safeFile = gettingInfo else {
print("Error: Getting Info is nil")
completion(false)
return
}
self.parseJSON(datenEintragen: safeFile)
completion(true)
}
task.resume()
} else {
//can't create URL
completion(false)
}
}
Now, in your second view controller, call this func like this:
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.linkZusammenfuegen { [weak self] successful in
guard let self = self else { return }
DispatchQueue.main.async {
if successful {
self.werteEintragen()
} else {
//do something else
}
}
}
}
I highly recommend Google's Promises Framework:
https://github.com/google/promises/blob/master/g3doc/index.md
It is well explained and documented. The basic concept works like this:
import Foundation
import Promises
struct DataFromServer {
var name: String
//.. and more data fields
}
func fetchDataFromServer() -> Promise <DataFromServer> {
return Promise { fulfill, reject in
//Perform work
//This block will be executed asynchronously
//call fulfill() if your value is ready
//call reject() if an error occurred
fulfill(data)
}
}
func visualizeData(data: DataFromServer) {
// do something with data
}
func start() {
fetchDataFromServer
.then { dataFromServer in
visualizeData(data: dataFromServer)
}
}
The closure after "then" will always be executed after the previous Promise has been resolved, making it easy to fulfill asynchronous tasks in order.
This is especially helpful to avoid nested closures (pyramid of death), as you can chain promises instead.

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