Write simple test for api call - ios

I want to write a test for function that interact with API. I ended up with:
class FileDownloaderTests: XCTestCase {
// MARK: timeouts
let regularTimeout: TimeInterval = 10
let largeTimeout: TimeInterval = 15
func testDownload() {
// URLS.firstFileUrl.rawValue
let downloader = FileDownloader(string: URLS.firstFileUrl.rawValue)
downloader.download(successCompletion: {
XCTAssertTrue(true)
}) { error in
print("error in test - \(error)")
}
waitForExpectations(timeout: largeTimeout, handler: nil)
}
}
So, it suppose to wait largeTimeout(15 seconds) for successCompletion closure, then test should be passed. But it ended up with an error:
*** Assertion failure in -[FileDownloaderTests.FileDownloaderTests waitForExpectationsWithTimeout:handler:], /Library/Caches/com.apple.xbs/Sources/XCTest_Sim/XCTest-14460.20/Sources/XCTestFramework/Async/XCTestCase+AsynchronousTesting.m:28
/Users/Necrosoft/Documents/Programming/Work/Life-Pay/FileDownloader/FileDownloaderTests/FileDownloaderTests.swift:28: error: -[FileDownloaderTests.FileDownloaderTests testDownload] : failed: caught "NSInternalInconsistencyException", "API violation - call made to wait without any expectations having been set."

You need to fulfill the expectation to tell the expectation that it can stop waiting/the process has finished
func testDownload() {
// URLS.firstFileUrl.rawValue
let downloader = FileDownloader(string: URLS.firstFileUrl.rawValue)
downloader.download(successCompletion: {
XCTAssertTrue(true)
expectation.fulfill()
}) { error in
print("error in test - \(error)")
expectation.fulfill()
}
waitForExpectations(timeout: largeTimeout, handler: nil)
}
Note: it is generally not a good idea to run automated tests against a live API. You should either use a stubbed response to just test that your handling of the code is correct or at least test against a test/staging API.
EDIT: you have two completion handlers so I called fulfill in each

use below example to create your own test
func testLogin() throws {
let expectation = XCTestExpectation(description: "DeviceID register with URL")
NetworkAPI.shared.loginRequest(username: "zdravko.zdravkin", password: "password") { authenticated in
switch authenticated {
case true:
XCTAssertTrue(true, "authenticated")
case false:
XCTFail("wrong username, password or deviceID")
}
}
wait(for: [expectation], timeout: 10.0)
}

Related

DispatchGroup logical workflow

I am trying to implement DispatchGroup as follows, but if the first call returns true, then the second one returns false, then overall result will return false.
However, if the first call returns false, then the second one returns true, then overall result will return false which is not what I expected.
I want to return false, if any of the call returns false. How could I able to handle this issue?
func storeInformation(id: String?, _ completion: #escaping (Bool) -> ()) {
guard
let id = id
else {
completion(false)
return
}
let dispatchGroup = DispatchGroup()
var groupResult: Bool = false
dispatchGroup.enter()
storeFeatures { success in
if success {
groupResult = true
} else {
groupResult = false
}
dispatchGroup.leave()
}
dispatchGroup.enter()
storeClasses { success in
if success {
groupResult = true
} else {
groupResult = false
}
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
completion(groupResult)
}
}
private func storeClasses(_ completion: #escaping(Bool) -> Void) {
postClasses { (error) in
if let _ = error {
completion(false)
} else {
completion(true)
}
}
}
private func storeFeatures(_ completion: #escaping(Bool) -> Void) {
postFeatures { (error) in
if let _ = error {
completion(false)
} else {
completion(true)
}
}
}
If we look at your storeClasses and storeFeatures, we see that they are not really actions that return a Bool; they are inherently attempts to post something that can fail. Hence what you really want to know is not whether something returned true or false but whether or not it failed. That is what you really mean — and it is always better, in programming, to say what you mean.
Using the Combine framework, we can express that sort of behavior with unbelievable succinctness. When we have multiple asynchronous actions to perform simultaneously, that is a Merge. And if one of them fails, the entire Merge fails. In other words, the very thing you want to do is effectively automatic!
Imagine, for example, that we have expressed your post actions by wrapping them in deferred Futures of type <Void,Error>. And suppose we have methods storeClassesFuture and storeFeaturesFuture that produce those Futures. Then all you have to say is:
Publishers.Merge(storeClassesFuture(), storeFeaturesFuture())
That is literally all there is to it! If you subscribe to that Merge with a sink, then either it receives a finished completion or a failure completion. And guess what? It receives the failure completion if and only if one or both of the post actions failed! It receives the finished completion only if they both succeeded, which is exactly what you want to know.
As a test bed, here's a sample implementation of your storeInformation (I'm ignoring the String for purposes of the example):
var storage = Set<AnyCancellable>()
enum Oops : Error { case darn }
func storeInformation() {
Publishers.Merge(storeClassesFuture(), storeFeaturesFuture())
.receive(on: DispatchQueue.main)
.sink { (completion) in
switch completion {
case .failure: print("at least one of them failed")
case .finished: print("they both succeeded")
}
print("---")
} receiveValue: { _ in }
.store(in: &storage)
}
And just to act as a random test, here are two futures that can randomly succeed or fail:
func storeClassesFuture() -> AnyPublisher<Void,Error> {
Deferred {
Future<Void,Error> { promise in
if Bool.random() {
print("storeClassesFuture succeeded")
promise(.success(()))
} else {
print("storeClassesFuture failed")
promise(.failure(Oops.darn))
}
}
}.eraseToAnyPublisher()
}
func storeFeaturesFuture() -> AnyPublisher<Void,Error> {
Deferred {
Future<Void,Error> { promise in
if Bool.random() {
print("storeFeaturesFuture succeeded")
promise(.success(()))
} else {
print("storeFeaturesFuture failed")
promise(.failure(Oops.darn))
}
}
}.eraseToAnyPublisher()
}
And here's some sample output from calling storeInformation repeatedly:
storeClassesFuture succeeded
storeFeaturesFuture succeeded
they both succeeded
---
storeClassesFuture failed
storeFeaturesFuture failed
at least one of them failed
---
storeClassesFuture failed
storeFeaturesFuture succeeded
at least one of them failed
---
storeClassesFuture failed
storeFeaturesFuture failed
at least one of them failed
---
storeClassesFuture failed
storeFeaturesFuture succeeded
at least one of them failed
---
storeClassesFuture succeeded
storeFeaturesFuture succeeded
they both succeeded
---
storeClassesFuture succeeded
storeFeaturesFuture succeeded
they both succeeded
---
storeClassesFuture failed
storeFeaturesFuture succeeded
at least one of them failed
---
storeClassesFuture failed
storeFeaturesFuture succeeded
at least one of them failed
---
storeClassesFuture succeeded
storeFeaturesFuture succeeded
they both succeeded
---
As you can see, the logic you're after is perfectly expressed by the Merge of two failable Futures.
(This sort of thing is a very good reason to adopt the Combine framework instead of using DispatchGroup. I find that everything I used to do with DispatchGroup can be done better with Combine. This just happens to be a particularly clearcut instance.)
You have an "AND" semantic here, so you should write that in your code:
let dispatchGroup = DispatchGroup()
var groupResult: Bool = true // identity for AND
dispatchGroup.enter()
storeFeatures { success in
groupResult = groupResult && success // here!
dispatchGroup.leave()
}
dispatchGroup.enter()
storeClasses { success in
groupResult = groupResult && success // and here
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
completion(groupResult)
}
When each task finishes, you want to express the idea that
The group result should be true iff the previous group result is true AND success is true

Combine Future block called multiple times when using Flatmap and multiple subscribers

I've been successfully using BrightFutures in my apps mainly for async network requests. I decided it was time to see if I could migrate to Combine. However what I find is that when I combine two Futures using flatMap with two subscribers my second Future code block is executed twice. Here's some example code which will run directly in a playground:
import Combine
import Foundation
extension Publisher {
func showActivityIndicatorWhileWaiting(message: String) -> AnyCancellable {
let cancellable = sink(receiveCompletion: { _ in Swift.print("Hide activity indicator") }, receiveValue: { (_) in })
Swift.print("Busy: \(message)")
return cancellable
}
}
enum ServerErrors: Error {
case authenticationFailed
case noConnection
case timeout
}
func authenticate(username: String, password: String) -> Future<Bool, ServerErrors> {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}
func downloadUserInfo(username: String) -> Future<String, ServerErrors> {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> some Publisher {
return authenticate(username: username, password: password).flatMap { (isAuthenticated) -> Future<String, ServerErrors> in
guard isAuthenticated else {
return Future {$0(.failure(.authenticationFailed)) }
}
return downloadUserInfo(username: username)
}
}
let future = authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
let cancellable2 = future.showActivityIndicatorWhileWaiting(message: "Please wait downloading")
let cancellable1 = future.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) { (output) in
print("received userInfo: '\(output)'")
}
The code simulates making two network calls and flatmaps them together as a unit which either succeeds or fails.
The resulting output is:
Calling server to authenticate
Busy: Please wait downloading
Downloading user info
Downloading user info <---- unexpected second network call
Hide activity indicator
received userInfo: 'decoded user data'
Completed without errors.
The problem is downloadUserInfo((username:) appears to be called twice. If I only have one subscriber then downloadUserInfo((username:) is only called once. I have an ugly solution that wraps the flatMap in another Future but feel I missing something simple. Any thoughts?
When you create the actual publisher with let future, append the .share operator, so that your two subscribers subscribe to a single split pipeline.
EDIT: As I've said in my comments, I'd make some other changes in your pipeline. Here's a suggested rewrite. Some of these changes are stylistic / cosmetic, as an illustration of how I write Combine code; you can take it or leave it. But other things are pretty much de rigueur. You need Deferred wrappers around your Futures to prevent premature networking (i.e. before the subscription happens). You need to store your pipeline or it will go out of existence before networking can start. I've also substituted a .handleEvents for your second subscriber, though if you use the above solution with .share you can still use a second subscriber if you really want to. This is a complete example; you can just copy and paste it right into a project.
class ViewController: UIViewController {
enum ServerError: Error {
case authenticationFailed
case noConnection
case timeout
}
var storage = Set<AnyCancellable>()
func authenticate(username: String, password: String) -> AnyPublisher<Bool, ServerError> {
Deferred {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}.eraseToAnyPublisher()
}
func downloadUserInfo(username: String) -> AnyPublisher<String, ServerError> {
Deferred {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}.eraseToAnyPublisher()
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> AnyPublisher<String, ServerError> {
let authenticate = self.authenticate(username: username, password: password)
let pipeline = authenticate.flatMap { isAuthenticated -> AnyPublisher<String, ServerError> in
if isAuthenticated {
return self.downloadUserInfo(username: username)
} else {
return Fail<String, ServerError>(error: .authenticationFailed).eraseToAnyPublisher()
}
}
return pipeline.eraseToAnyPublisher()
}
override func viewDidLoad() {
super.viewDidLoad()
authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
.handleEvents(
receiveSubscription: { _ in print("start the spinner!") },
receiveCompletion: { _ in print("stop the spinner!") }
).sink(receiveCompletion: {
switch $0 {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) {
print("received userInfo: '\($0)'")
}.store(in: &self.storage)
}
}
Output:
start the spinner!
Calling server to authenticate
Downloading user info
received userInfo: 'decoded user data'
stop the spinner!
Completed without errors.

How to remove callback option UI of CallKit after ending the call

In my app i'm using CallKit for incoming call. There is no outgoing call feature in the app. Everything is fine but when the receiver or dailer ends the call it shows the CallKit UI with call back option. I don't want to show callback option, how can I do it?
My code for ending the call
func end(call: SpeakerboxCall) {
let endCallAction = CXEndCallAction(call: call.uuid)
let transaction = CXTransaction()
transaction.addAction(endCallAction)
requestTransaction(transaction, action: "endCall")
}
private func requestTransaction(_ transaction: CXTransaction, action:
String = "") {
callController.request(transaction) { error in
if let error = error {
print("Error requesting transaction: \(error)")
} else {
print("Requested transaction \(action) successfully")
}
}
}
I have solved it. I was force quitting the CallKit where the transaction is not correctly completing.
AppDelegate.shared.providerDelegate?.provider.reportCall(with: call.uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded)
We need to set endedAt to nil and reason to remoteEnded
Close All Call (Swift4)
func performEndCallAction() {
for call in self.cxCallController.callObserver.calls {
let endCallAction = CXEndCallAction(call: call.uuid)
let transaction = CXTransaction(action: endCallAction)
cxCallController.request(transaction) { error in
if let error = error {
NSLog("EndCallAction transaction request failed: \(error.localizedDescription).")
return
}
NSLog("EndCallAction transaction request successful")
}
}
}

HKObserverQuery only runs when the application is reopened

So I've been following the instructions in this answer...
Healthkit background delivery when app is not running
The code runs fine and works whilst the application is open and says that background delivery is successful, however when I test the application by walking around and changing the clock on the device to an hour forward I do not receive any logs to let me know it has run. However, if I open the application again the observer query runs.
private func checkAuthorization(){
let healthDataToRead = Set(arrayLiteral: self.distanceQuantityType!)
healthKitStore.requestAuthorization(toShare: nil, read: healthDataToRead) { (success, error) in
if error != nil {
print(error?.localizedDescription)
print("There was an error requesting Authorization to use Health App")
}
if success {
print("success")
}
}
}
public func enableBackgroundDelivery() {
self.checkAuthorization()
self.healthKitStore.enableBackgroundDelivery(for: self.distanceQuantityType!, frequency: .hourly) { (success, error) in
if success{
print("Background delivery of steps. Success = \(success)")
}
if let error = error {
print("Background delivery of steps failed = \(error.localizedDescription)")
}
}
}
func observeDistance(_ handler:#escaping (_ distance: Double) -> Void) {
let updateHandler: (HKObserverQuery?, HKObserverQueryCompletionHandler?, Error?) -> Void = { query, completion, error in
if !(error != nil) {
print("got an update")
completion!()
} else {
print("observer query returned error: \(error)")
}
}
let query = HKObserverQuery(sampleType: self.distanceQuantityType!, predicate: nil, updateHandler: updateHandler)
self.healthKitStore.execute(query)
}
The query is initialised in the appDelegate method didFinishLaunching
This particular HealthKitQuery is asynchronous. You should wait until it finishes processing.
However, this case is not possible in didFinishLaunching. The application just ended execution and there is not enough time to process the query.
I would seriously suggest to rethink the logic behind the operation of your code. A good way to solve this would be to put the request elsewhere, preferrably after the needed operations were completed.

API violation when using waitForExpectations

I'm running a UI test where I need to test an asynchronous function using the waitForExpectations API.
I'm getting this error:
"NSInternalInconsistencyException", "API violation - call made to wait without any expectations having been set."
I really don't understand, as I have correctly created the expectation.
Also, there seems to be a documentation bug: according to the documentation the API is expectation(description:) but the compiler won't accept that, instead I need to use XCTestExpectation() to create one.
func testExample() {
XCTAssertTrue(state == .STATE_NOT_READY)
let exp1 = XCTestExpectation()
let queue = DispatchQueue(label: "net.tech4freedom.AppTest")
let delay: DispatchTimeInterval = .seconds((2))
queue.asyncAfter(deadline: .now() + delay) {
XCTAssertTrue(true)
exp1.fulfill()
}
self.waitForExpectations(timeout: 4){ [weak self] error in
print("X: async expectation")
XCTAssertTrue(true)
}
self.waitForExpectations(timeout: 10.0, handler: nil)
}
Ok, your mistake is that you try to instantiate the expectation directly. The docs clearly say
Use the following XCTestCase methods to create XCTestExpectation instances:
- expectation(description:)
This means, that you should create expectations like this :
func testMethod() {
let exp = self.expectation(description: "myExpectation")
// your test code
}

Resources