How To UnitTest Combine Cancellables? - swift

How to write unit test for this func loadDemos()?
Here is the code
class BenefitViewModel: ObservableObject {
func loadDemos() {
let testMode = ProcessInfo.processInfo.arguments.contains("testMode")
if testMode {
self.demos = DummyData().decodeDemos()
} else {
cancellables.insert(self.getDemos().sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print(error.localizedDescription)
break
case .finished:
break
}
}, receiveValue: { response in
self.demos = response
print(“Demos: \(response.count)")
}))
}
}
}

At the risk of being a little annoying, I'm going to answer the more general version of your question: how can you unit test a Combine pipeline?
Let's step back and start with some general principles about unit testing:
Don't test Apple's code. You already know what it does. Test your code.
Don't test the network (except in a rare test where you just want to make sure the network is up). Substitute your own class that behaves like the network.
Asynchronous code needs asynchronous testing.
I assume your getDemos does some asynchronous networking. So without loss of generality I can illustrate with a different pipeline. Let's use a simple Combine pipeline that fetches an image URL from the network and stores it in a UIImage instance property (this is intended to be quite parallel to what you are doing with your pipeline response and self.demos). Here's a naive implementation (assume that I have some mechanism for calling fetchImage):
class ViewController: UIViewController {
var image : UIImage?
var storage = Set<AnyCancellable>()
func fetchImage() {
let url = URL(string:"https://www.apeth.com/pep/manny.jpg")!
self.getImageNaive(url:url)
}
func getImageNaive(url:URL) {
URLSession.shared.dataTaskPublisher(for: url)
.compactMap { UIImage(data:$0.data) }
.receive(on: DispatchQueue.main)
.sink { completion in
print(completion)
} receiveValue: { [weak self] image in
print(image)
self?.image = image
}
.store(in: &self.storage)
}
}
All very nice, and it works fine, but it isn't testable. The reason is that if we simply call getImageNaive in our test, we will be testing the network, which is unnecessary and wrong.
So let's make this testable. How? Well, in this simple example, we just need to break off the asynchronous publisher from the rest of the pipeline, so that the test can substitute its own publisher that doesn't do any networking. So, for example (again, assume I have some mechanism for calling fetchImage):
class ViewController: UIViewController {
// Output is (data: Data, response: URLResponse)
// Failure is URLError
typealias DTP = AnyPublisher <
URLSession.DataTaskPublisher.Output,
URLSession.DataTaskPublisher.Failure
>
var image : UIImage?
var storage = Set<AnyCancellable>()
func fetchImage() {
let url = URL(string:"https://www.apeth.com/pep/manny.jpg")!
self.getImage(url:url)
}
func getImage(url:URL) {
let pub = self.dataTaskPublisher(for: url)
self.createPipelineFromPublisher(pub: pub)
}
func dataTaskPublisher(for url: URL) -> DTP {
URLSession.shared.dataTaskPublisher(for: url).eraseToAnyPublisher()
}
func createPipelineFromPublisher(pub: DTP) {
pub
.compactMap { UIImage(data:$0.data) }
.receive(on: DispatchQueue.main)
.sink { completion in
print(completion)
} receiveValue: { [weak self] image in
print(image)
self?.image = image
}
.store(in: &self.storage)
}
}
You see the difference? It's almost the same, but the pipeline itself is now distinct from the publisher. Our method createPipelineFromPublisher takes as its parameter any publisher of the correct type. This means that we have abstracted out the use of URLSession.shared.dataTaskPublisher, and can substitute our own publisher. In other words, createPipelineFromPublisher is testable!
Okay, let's write the test. My test case contains a method that generates a "mock" publisher that simply publishes some Data wrapped up in the same publisher type as a data task publisher:
func dataTaskPublisherMock(data: Data) -> ViewController.DTP {
let fakeResult = (data, URLResponse())
let j = Just<URLSession.DataTaskPublisher.Output>(fakeResult)
.setFailureType(to: URLSession.DataTaskPublisher.Failure.self)
return j.eraseToAnyPublisher()
}
My test bundle (which is called CombineTestingTests) also has an asset catalog containing a UIImage called mannyTesting. So all I have to do is call ViewController's createPipelineFromPublisher with the data from that UIImage, and check that the ViewController's image property is now that same image, right?
func testImagePipeline() throws {
let vc = ViewController()
let mannyTesting = UIImage(named: "mannyTesting", in: Bundle(for: CombineTestingTests.self), compatibleWith: nil)!
let data = mannyTesting.pngData()!
let pub = dataTaskPublisherMock(data: data)
vc.createPipelineFromPublisher(pub: pub)
let image = try XCTUnwrap(vc.image, "The image is nil")
XCTAssertEqual(data, image.pngData()!, "The image is the wrong image")
}
Wrong! The test fails; vc.image is nil. What went wrong? The answer is that Combine pipelines, even a pipeline that starts with a Just, are asynchronous. Asynchronous pipelines require asynchronous testing. My test needs to wait until vc.image is not nil. One way to do that is with a predicate that watches for vc.image to no longer be nil:
func testImagePipeline() throws {
let vc = ViewController()
let mannyTesting = UIImage(named: "mannyTesting", in: Bundle(for: CombineTestingTests.self), compatibleWith: nil)!
let data = mannyTesting.pngData()!
let pub = dataTaskPublisherMock(data: data)
vc.createPipelineFromPublisher(pub: pub)
let pred = NSPredicate { vc, _ in (vc as? ViewController)?.image != nil }
let expectation = XCTNSPredicateExpectation(predicate: pred, object: vc)
self.wait(for: [expectation], timeout: 10)
let image = try XCTUnwrap(vc.image, "The image is nil")
XCTAssertEqual(data, image.pngData()!, "The image is the wrong image")
}
And the test passes! Do you see the point? The system-under-test here is exactly the right thing, namely, the mechanism that forms a pipeline that receives the output that a data task publisher would emit and sets an instance property of our view controller. We have tested our code and only our code. And we have demonstrated that our pipeline works correctly.

With #Joakim Danielson help on this question How to convert myObject to AnyPublisher<myObject, Never>?. I came up with this answer.
func testDemoData() {
let testDemoData = Just(demoData).eraseToAnyPublisher()
cancellables.insert(testDemoData.sink(receiveCompletion: { [weak self] result in
switch result {
case .failure(let error):
XCTAssert(true)
print(error)
break
case .finished:
break
}
}, receiveValue: { [weak self] response in
XCTAssert((response!.count ) >= 0)
}))
}

Related

How to wait until data from network call comes and only then return value of a function #Swift

I have a service class that makes an api call and stores data into its property. Then my interactor class have a method where I want to make service class api call and when data will be stored - return it. I tried myself to handle this with completion handler and dispatch group, but (I suppose I just missing something) this didn't work. I would be very appreciated if you help me to deal with this problem. Thanks in advance!
Service class:
class PunkApiService{
var beers = [Beer]()
func loadList(at page: Int){
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
guard error == nil else {
print(error!)
return
}
//MARK: - Handling data came
guard let data = data else{
print("Failed to load data")
return
}
do{
let beers = try JSONDecoder().decode([Beer].self, from: data)
self.beers.append(contentsOf: beers)
}
catch{
print("Failed to decode data")
}
}
task.resume()
}
And Interactor class(without completion handler or dispatch group):
class BeersListInteractor:BeersListInteractorProtocol{
private var favoriteBeers = FavoriteBeers()
private var service = PunkApiService()
//MARK: - Load list of Beers
func loadList(at page: Int) -> [Beer]{
service.loadList(at: page)
return service.beers
}
Added: my attempt with completion handler
var beers: [Beer]
func loadList(at page: Int, completion: ()->()){
service.loadList(at: page)
completion()
}
func completion(){
beers.append(contentsOf: service.beers)
}
loadList(at: 1) {
completion()
}
This is what async/await pattern is for, described here. In your case both loadList functions are async, and the second one awaits for the first one:
class PunkApiService {
func loadList(at page: Int) async {
// change function to await for task result
let (data, error) = try await URLSession.shared.data(from: url)
let beers = try JSONDecoder().decode([Beer].self, from: data)
...
return beers
}
}
class BeersListInteractor: BeersListInteractorProtocol {
func loadList(at page: Int) async -> [Beer]{
let beers = await service.loadList(at: page)
return service.beers
}
}
See a good explanation here
I think that you were on the right path when attempting to use a completion block, just didn't do it correctly.
func loadList(at page: Int, completion: #escaping ((Error?, Bool, [Beer]?) -> Void)) {
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
completion(nil, false, nil)
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
if let error = error {
completion(error, false, nil)
print(error!)
return
}
//MARK: - Handling data came
guard let data = data, let beers = try? JSONDecoder().decode([Beer].self, from: data) else {
completion(nil, false, nil)
return
}
completion(nil, true, beers)
}
task.resume()
}
This is the loadList function, which now has a completion parameter that will have three parameters, respectively the optional Error, the Bool value representing success or failure of obtaining the data, and the actual [Beers] array, containing the data (if any was retrieved).
Here's how you would now call the function:
service.loadList(at: page) { error, success, beers in
if let error = error {
// Handle the error here
return
}
if success, let beers = beers {
// Data was correctly retrieved - and safely unwrapped for good measure, do what you need with it
// Example:
loader.stopLoading()
self.datasource = beers
self.tableView.reloadData()
}
}
Bear in mind the fact that the completion is being executed asynchronously, without stopping the execution of the rest of your app.
Also, you should decide wether you want to handle the error directly inside the loadList function or inside the closure, and possibly remove the Error parameter if you handle it inside the function.
The same goes for the other parameters: you can decide to only have a closure that only has a [Beer] parameter and only call the closure if the data is correctly retrieved and converted.

swift wait until the urlsession finished

each time i call this api https://foodish-api.herokuapp.com/api/ i get an image. I don't want one image, i need 11 of them, so i made the loop to get 11 images.
But what i can't do is reloading the collection view once the loop is finish.
func loadImages() {
DispatchQueue.main.async {
for _ in 1...11{
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
}
self.collectionV.reloadData()
print("after resume")
}
Typically, when we want to know when a series of concurrent tasks (such as these network requests) are done, we would reach for a DispatchGroup. Call enter before the network request, call leave in the completion handler, and specify a notify block, e.g.
/// Load images
///
/// - Parameter completion: Completion handler to return array of URLs. Called on main queue
func loadImages(completion: #escaping ([URL]) -> Void) {
var imageURLs: [Int: URL] = [:] // note, storing results in local variable, avoiding need to synchronize with property
let group = DispatchGroup()
let count = 11
for index in 0..<count {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
defer { group.leave() }
guard let data = data else { return }
do {
let foodImage = try JSONDecoder().decode(FoodImage.self, from: data)
imageURLs[index] = foodImage.url
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) {
let sortedURLs = (0..<count).compactMap { imageURLs[$0] }
completion(sortedURLs)
}
}
Personally, rather than JSONSerialization, I use JSONDecoder with a Decodable type to parse the JSON response. (Also, I find the key name, image, to be a bit misleading, so I renamed it to url to avoid confusion, to make it clear it is a URL for the image, not the image itself.) Thus:
struct FoodImage: Decodable {
let url: URL
enum CodingKeys: String, CodingKey {
case url = "image"
}
}
Also note that the above is not updating properties or reloading the collection view. A routine that is performing network requests should not also be updating the model or the UI. I would leave this in the hands of the caller, e.g.,
var imageURLs: [URL]?
override func viewDidLoad() {
super.viewDidLoad()
// caller will update model and UI
loadImages { [weak self] imageURLs in
self?.imageURLs = imageURLs
self?.collectionView.reloadData()
}
}
Note:
The DispatchQueue.main.async is not necessary. These requests already run asynchronously.
Store the temporary results in a local variable. (And because URLSession uses a serial queue, we do not have to worry about further synchronization.)
The dispatch group notify block, though, uses the .main queue, so that the caller can conveniently update properties and UI directly.
Probably obvious, but I am parsing the URL directly, rather than parsing a string and converting that to a URL.
When fetching results concurrently, you have no assurances regarding the order in which they will complete. So, one will often capture the results in some order-independent structure (such as a dictionary) and then sort the results before passing it back.
In this particular case, the order doesn't strictly matter, but I included this sort-before-return pattern in my above example, as it is generally the desired behavior.
Anyway, that yields:
If you want to get one reload after finish loading of all 11 images you need to use DispatchGroup. Add a property that create a group:
private let group = DispatchGroup()
Then modify your loadImages() function:
func loadImages() {
for _ in 1...11 {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
self.group.leave()
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) { [weak self] in
self?.collectionV.reloadData()
}
}
Some description:
On the method call group.enter() will be called 11 times
On each completion of image downloading group.leave() will be called
When group.leave() will be called the same count like group.enter() group make call of the block that you defined in group.notify()
More about DispatchGroup
Notice that you need handle create and store different DispatchGroup object if you need to download different groups of images in the same time.

Asynchronous thread in Swift - How to handle?

I am trying to recover a data set from a URL (after parsing a JSON through the parseJSON function which works correctly - I'm not attaching it in the snippet below).
The outcome returns nil - I believe it's because the closure in retrieveData function is processed asynchronously. I can't manage to have the outcome saved into targetData.
Thanks in advance for your help.
class MyClass {
var targetData:Download?
func triggerEvaluation() {
retrieveData(url: "myurl.com") { downloadedData in
self.targetData = downloadedData
}
print(targetData) // <---- Here is where I get "nil"!
}
func retrieveData(url: String, completion: #escaping (Download) -> ()) {
let myURL = URL(url)!
let mySession = URLSession(configuration: .default)
let task = mySession.dataTask(with: myURL) { [self] (data, response, error) in
if error == nil {
if let fetchedData = data {
let safeData = parseJSON(data: fetchedData)
completion(safeData)
}
} else {
//
}
}
task.resume()
}
}
Yes, it’s nil because retrieveData runs asynchronously, i.e. the data hasn’t been retrieved by the time you hit the print statement. Move the print statement (and, presumably, all of the updating of your UI) inside the closure, right where you set self.targetData).
E.g.
func retrieveData(from urlString: String, completion: #escaping (Result<Download, Error>) -> Void) {
let url = URL(urlString)!
let mySession = URLSession.shared
let task = mySession.dataTask(with: url) { [self] data, response, error in
guard
let responseData = data,
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknown(response, data))
}
return
}
let safeData = parseJSON(data: responseData)
DispatchQueue.main.async {
completion(.success(safeData))
}
}
task.resume()
}
Where
enum NetworkError: Error {
case unknown(URLResponse?, Data?)
}
Then the caller would:
func triggerEvaluation() {
retrieveData(from: "https://myurl.com") { result in
switch result {
case .failure(let error):
print(error)
// handle error here
case .success(let download):
self.targetData = download
// update the UI here
print(download)
}
}
// but not here
}
A few unrelated observations:
You don't want to create a new URLSession for every request. Create only one and use it for all requests, or just use shared like I did above.
Make sure every path of execution in retrieveData calls the closure. It might not be critical yet, but when we write asynchronous code, we always want to make sure that we call the closure.
To detect errors, I'd suggest the Result pattern, shown above, where it is .success or .failure, but either way you know the closure will be called.
Make sure that model updates and UI updates happen on the main queue. Often, we would have retrieveData dispatch the calling of the closure to the main queue, that way the caller is not encumbered with that. (E.g. this is what libraries like Alamofire do.)

Implementing reconnection with URLSession publisher and Combine

I'm wondering if there is a way to implement reconnection mechanism with new Apple framework Combine and use of URLSession publisher
tried to find some examples in WWDC 2019
tried to play with waitsForConnectivity with no luck (it even not calling delegate on custom session)
tried URLSession.background but it crashed during publishing.
I'm also not understanding how do we track progress in this way
Does anyone already tried to do smth like this?
upd:
It seems like waitsForConnectivity is not working in Xcode 11 Beta
upd2:
Xcode 11 GM - waitsForConnectivity is working but ONLY on device. Use default session, set the flag and implement session delegate. Method task is waiting for connectivity will be invoked no matter if u r using init task with callback or without.
public class DriverService: NSObject, ObservableObject {
public var decoder = JSONDecoder()
public private(set) var isOnline = CurrentValueSubject<Bool, Never>(true)
private var subs = Set<AnyCancellable>()
private var base: URLComponents
private lazy var session: URLSession = {
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
public init(host: String, port: Int) {
base = URLComponents()
base.scheme = "http"
base.host = host
base.port = port
super.init()
// Simulate online/offline state
//
// let pub = Timer.publish(every: 3.0, on: .current, in: .default)
// pub.sink { _ in
// let rnd = Int.random(in: 0...1)
// self.isOnline.send(rnd == 1)
// }.store(in: &subs)
// pub.connect()
}
public func publisher<T>(for driverRequest: Request<T>) -> AnyPublisher<T, Error> {
var components = base
components.path = driverRequest.path
var request = URLRequest(url: components.url!)
request.httpMethod = driverRequest.method
return Future<(data: Data, response: URLResponse), Error> { (complete) in
let task = self.session.dataTask(with: request) { (data, response, error) in
if let err = error {
complete(.failure(err))
} else {
complete(.success((data!, response!)))
}
self.isOnline.send(true)
}
task.resume()
}
.map({ $0.data })
.decode(type: T.self, decoder: decoder)
.eraseToAnyPublisher()
}
}
extension DriverService: URLSessionTaskDelegate {
public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
self.isOnline.send(false)
}
}
Have you tried retry(_:) yet? It’s available on Publishers and reruns the request upon failure.
If you don’t want the request to immediately rerun for all failures then you can use catch(_:) and decide which failures warrant a rerun.
Here's some code to achieve getting the progress.
enum Either<Left, Right> {
case left(Left)
case right(Right)
var left: Left? {
switch self {
case let .left(value):
return value
case .right:
return nil
}
}
var right: Right? {
switch self {
case let .right(value):
return value
case .left:
return nil
}
}
}
extension URLSession {
func dataTaskPublisherWithProgress(for url: URL) -> AnyPublisher<Either<Progress, (data: Data, response: URLResponse)>, URLError> {
typealias TaskEither = Either<Progress, (data: Data, response: URLResponse)>
let completion = PassthroughSubject<(data: Data, response: URLResponse), URLError>()
let task = dataTask(with: url) { data, response, error in
if let data = data, let response = response {
completion.send((data, response))
completion.send(completion: .finished)
} else if let error = error as? URLError {
completion.send(completion: .failure(error))
} else {
fatalError("This should be unreachable, something is clearly wrong.")
}
}
task.resume()
return task.publisher(for: \.progress.completedUnitCount)
.compactMap { [weak task] _ in task?.progress }
.setFailureType(to: URLError.self)
.map(TaskEither.left)
.merge(with: completion.map(TaskEither.right))
.eraseToAnyPublisher()
}
}
I read your question title several times. If you mean reconnect the URLSession's publisher. Due to the URLSession.DataTaskPublisher has two results. Success output or Failure (a.k.a URLError). It's not possible to make it reconnect after the output produced.
You can declare one subject. e.g
let output = CurrentValueSubject<Result<T?, Error>, Never>(.success(nil))
And add a trigger when network connection active then request resources and send the new Result to the output. Subscribe output in the other place. So that you can get new value when network back-online.

swift - order of functions - which code runs when?

I have an issue with my code and I think it could be related to the order in which code is called.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
private var tasks = [Task]()
override func willActivate() {
let taskUrl = "http://myjsonurl.com"
downloadJsonTask(url: taskUrl)
print(tasks.count) // EMPTY
super.willActivate()
}
func downloadJsonTask(url: String) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
return
}
do
{
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("somehting went wrong after downloading")
}
}.resume()
}
}
I'm defining the private var tasks and fill it with the downloadJsonTask function but after the function ran the print(tasks.count) gives 0.
When I call print(downloadedTasks.tasks.count) it gives 4.
I think that in sequence of time the tasks variable is empty when I print it and it is filled later on.
When you are trying to print number of tasks in willActivate(), function downloadJsonTask(url: String) hasn't been completed yet, so you have empty array because tasks haven't been set yet.
You should add completion handler to downloadJsonTask just like this:
(don't forget to pass completion as parameter of function)
func downloadJsonTask(url: String, completion: #escaping () -> Void) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
completion()
return
}
do {
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("something went wrong after downloading")
}
completion() // This is moment when code which you write inside closure get executed
}.resume()
}
In your willActivate() use this function like this:
downloadJsonTask(url: taskUrl) {
print(tasks.count)
}
So that means when you get your data, your code inside curly braces will get executed.
You’re correct in your assumption that tasks has not yet been assigned a value when it’s first printed.
The thing is network requests are performed asynchronously. It means that iOS does not wait until downloadJsonTask(url:) is finished but continues executing the code right away (i.e. it calls print(tasks.count) immediately after the network request started, without waiting for it to produce any results).
The piece of code inside brackets after URLSession.shared.dataTask(with:) is called a completion handler. This code gets executed once the network request is competed (hence the name). The tasks variable is assigned a value only when the request is finished. You can make sure it works by adding print(self.tasks.count) after self.tasks = downloadedTasks.tasks:
self.tasks = downloadedTasks.tasks
print(self.tasks)
print(downloadedTasks.tasks.count)