So I'm using PromiseKit in my latest swift app to do most of the networking code, along with Alamofire. I'm trying to setup my promises to throw when my returns aren't what I desire - here's what the code looks like:
`
do{
firstly({
try DoStuff.doStuff()
}).then({ response in
self.array = response
}).error { error in
throw Error.GeneralError
print(error)
}
firstly({
try DoOtherThing.otherThing()
}).then({ response in
self.stuff = response
}).error{ error in
throw TransactionError.GeneralError
print(error)
}
} catch {
let alertController = UIAlertController(title: "Network Error", message: "Network error, please try again", preferredStyle: .Alert)
let OKAction = UIAlertAction(title: "OK", style: .Default) { (action) in
//
}
alertController.addAction(OKAction)
self.presentViewController(alertController, animated: true) {
//
}
}
`
This code works just hunky dory if I don't have the 'throw' statements in there - if I just print the error, or put my alert controller code in there, works as expected. But when I add the throw, I get an compiler red flag on the 'error' line that says Cannot call value of non function type 'ErrorType' Any thoughts? Thanks
The way you would do this with PromiseKit would be something like:
let stuff = firstly {
try DoStuff.doStuff()
}.then { response in
self.array = response
}
let otherStuff = firstly {
try DoOtherThing.otherThing()
}.then { response in
self.stuff = response
}
when(fulfilled: stuff, otherStuff).catch { _ in
let alertController = UIAlertController(title: "Network Error", message: "Network error, please try again", preferredStyle: .alert)
let OKAction = UIAlertAction(title: "OK", style: .default) { (action) in
//
}
alertController.addAction(OKAction)
self.present(alertController, animated: true) {
//
}
}
In the above, I'm assuming that doStuff() and doOtherThing() are both synchronous functions that throw on error. As such, it doesn't make a lot of sense to wrap them in promises unless you are using the results to feed an asynchronous task and then using the result from that.
I think your understanding of do/catch isn't quite right.
Do/Catch is a synchronous operation only, so to catch the throw, the error must be thrown whilst in the do block. In this case, all you're doing inside the do block is setting up the promise. Should the error condition ever be reached, it will be executed asynchronously in a different context - outside of your do catch block and so cannot be caught.
EDIT:
To make it clearer why you are getting the error, here is the method signature for error in PromiseKit:
func error(policy policy: ErrorPolicy = .AllErrorsExceptCancellation, _ body: (ErrorType) -> Void)
The 'body' closure is not declared as throwing so therefore you cannot throw to exit that context. To throw, it would need to be declared like this:
func error(policy policy: ErrorPolicy = .AllErrorsExceptCancellation, _ body: (ErrorType) throws -> Void)
But it can't because it executes it asynchronously.
Related
The documentation for AVCaptureDeviceInput.init(device:) documents its parameters as:
device
The device from which to capture input.
outError
If an error occurs during initialization, upon return contains an NSError object describing the problem.
This outError out-parameter is, in Swift, represented as a thrown Error. I can catch and display this like so:
do {
let deviceInput = try AVCaptureDeviceInput(device: device)
// ...
}
catch {
print("Error: \(error)")
}
There is one specific case I want to handle gracefully: when the user has denied authorization for application to use the camera. In this case, I get the following output:
Error: Error Domain=AVFoundationErrorDomain Code=-11852 "Cannot use FaceTime HD Camera (Built-in)" UserInfo={NSLocalizedFailureReason=This app is not authorized to use FaceTime HD Camera (Built-in)., AVErrorDeviceKey=<AVCaptureDALDevice: 0x100520a60 [FaceTime HD Camera (Built-in)][0x8020000005ac8514]>, NSLocalizedDescription=Cannot use FaceTime HD Camera (Built-in)}
I need to distinguish this error type from other unexpected errors, like so:
do {
let deviceInput = try AVCaptureDeviceInput(device: device)
// ...
}
catch AVError.Code.applicationIsNotAuthorizedToUseDevice {
// Expected error, handle gracefully
errorMessageBox(errorText: "You have denied authorization to access your camera. Fix this in System Preferences > Security & Privacy.")
}
catch {
// Unexpected errors
errorMessageBox("Error: \(error)")
}
This is pseudocode and does not compile. I know that the error code -11852 is AVError.Code.applicationIsNotAuthorizedToUseDevice. However, I don't know how to get the error code out of the opaque error object in order to test it.
What is the specific type of the error thrown by AVCaptureDeviceInput.init(device:)? How do I extract the AVError.Code from it in order to handle this specific error?
There are two possible approaches. One is to check before you even attempt, e.g.
if AVCaptureDevice.authorizationStatus(for: .video) == .denied {
offerToOpenSettings()
return
}
The other approach is to catch the not authorized error:
let input: AVCaptureDeviceInput
do {
input = try AVCaptureDeviceInput(device: camera)
} catch AVError.applicationIsNotAuthorizedToUseDevice {
offerToOpenSettings()
return
} catch {
print("some other error", error)
return
}
Note, that’s catching AVError.applicationIsNotAuthorizedToUseDevice, not AVError.Code.applicationIsNotAuthorizedToUseDevice.
If, for example, this was an iOS app, you could have a function to offer to redirect the user to settings app:
func offerToOpenSettings() {
guard
let settings = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(settings)
else { return }
let alert = UIAlertController(title: nil, message: "Would you like to open Settings to enable permission to use the camera?", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
UIApplication.shared.open(settings)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
Note, since this is potentially presenting an alert, you don’t want to trigger this in viewDidLoad (which is too early in the process), but rather viewDidAppear.
Or, on macOS, maybe something like:
func offerToOpenSettings() {
let preferences = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera")!
let alert = NSAlert()
alert.messageText = #"The camera is disabled. Please go to the “Camera” section in Security System Preferences, and enable this app."#
alert.addButton(withTitle: "System Preferences")
alert.addButton(withTitle: "Cancel")
if alert.runModal() == .alertFirstButtonReturn {
NSWorkspace.shared.open(preferences)
}
}
I have a login view controller that user Almofire library to get the response. I do the unit test on that controller but the test always fail. I think because take time to response.
My test case:
override func setUp() {
super.setUp()
continueAfterFailure = false
let vc = UIStoryboard(name: "Main", bundle: nil)
controllerUnderTest = vc.instantiateViewController(withIdentifier: "LoginVC") as! LoginViewController
controllerUnderTest.loadView()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
controllerUnderTest = nil
super.tearDown()
}
func testLoginWithValidUserInfo() {
controllerUnderTest.email?.text = "raghad"
controllerUnderTest.pass?.text = "1234"
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}
I try to use:
waitForExpectations(timeout: 60, handler: nil)
But I got this error:
caught "NSInternalInconsistencyException"
almofire function in login presenter :
func sendRequest(withParameters parameters: [String : String]) {
Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in
debugPrint("new line : \(response)" )
switch response.result {
case .success(let value):
let userJSON = JSON(value)
self.readResponse(data: userJSON)
case .failure(let error):
print("Error \(String(describing: error))")
self.delegate.showMessage("* Connection issue ")
}
self.delegate.removeLoadingScreen()
//firebase log in
Auth.auth().signIn(withEmail: parameters["email"]!, password: parameters["pass"]!) { [weak self] user, error in
//guard let strongSelf = self else { return }
if(user != nil){
print("login with firebase")
}
else{
print("eroor in somthing")
}
if(error != nil){
print("idon now")
}
// ...
}
}
}
func readResponse(data: JSON) {
switch data["error"].stringValue {
case "true":
self.delegate.showMessage("* Invalid user name or password")
case "false":
if data["state"].stringValue=="0" {
self.delegate.showMessage("logged in successfully")
}else {
self.delegate.showMessage("* Inactive account")
}
default:
self.delegate.showMessage("* Connection issue")
}
}
How can I solve this problem? :(
Hi #Raghad ak, welcome to Stack Overflow 👋.
Your guess about the passage of time preventing the test to succeed is correct.
Networking code is asynchronous. After the test calls .sendActions(for: .touchUpInside) on your login button it moves to the next line, without giving the callback a chance to run.
Like #ajeferson's answer suggests, in the long run I'd recommend placing your Alamofire calls behind a service class or just a protocol, so that you can replace them with a double in the tests.
Unless you are writing integration tests in which you'd be testing the behaviour of your system in the real world, hitting the network can do you more harm than good. This post goes more into details about why that's the case.
Having said all that, here's a quick way to get your test to pass. Basically, you need to find a way to have the test wait for your asynchronous code to complete, and you can do it with a refined asynchronous expectation.
In your test you can do this:
expectation(
for: NSPredicate(
block: { input, _ -> Bool in
guard let label = input as? UILabel else { return false }
return label.text == "logged in successfully"
}
),
evaluatedWith: controllerUnderTest.lblValidationMessage,
handler: .none
)
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
waitForExpectations(timeout: 10, handler: nil)
That expectation will run the NSPredicate on a loop, and fulfill only when the predicate returns true.
You have to somehow signal to your tests that are safe to proceed (i.e. expectation is fulfilled). The ideal approach would be decouple that Alamofire code and mock its behavior when testing. But just to answer your question, you might want to do the following.
In your view controller:
func sendRequest(withParameters parameters: [String : String], completionHandler: (() -> Void)?) {
...
Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in
...
// Put this wherever appropriate inside the responseJSON closure
completionHandler?()
}
}
Then in your tests:
func testLoginWithValidUserInfo() {
controllerUnderTest.email?.text = "raghad"
controllerUnderTest.pass?.text = "1234"
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
let expectation = self.expectation(description: "logged in successfully)
waitForExpectations(timeout: 60, handler: nil)
controllerUnderTest.sendRequest(withParameters: [:]) {
expectation.fulfill()
}
XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}
I know you have some intermediate functions between the button click and calling the sendRequest function, but this is just for you to get the idea. Hope it helps!
There is a function that is triggered after the AccountKit authorization, it calls the Firebase Function to validate the token on Facebook and returns a userId if everything is confirmed, and registers the user if he has not yet been registered.
It works fine when Internet is available, but while offline - Firebase function does not return or throw any errors or at least nil results, and I would like it to return an error such as No internet connection or ANYTHING that could be catched.
Digging web and APIReference brought no results. Does the call of firebase function really not return anything in such cases (offline)?
func checkUserCredentials(phoneNumber: String, FBId: String, Token: String) {
functions.httpsCallable("checkUserCredentials").call(["phone":"\(phoneNumber)", "FBId":"\(FBId)", "Token":"\(Token)"])
{ (result, error) in
if let error = error as NSError?
{
if error.domain == FunctionsErrorDomain
{
let code = FunctionsErrorCode(rawValue: error.code)
let message = error.localizedDescription
}
}
if let userDoc = (result?.data as? [String: Any])?["userID"] as? String
{
DispatchQueue.main.async(execute: { self.performSegue(withIdentifier: "StartTheApp", sender: self) })
}
} }
I recommend checking for a network connection before making any network request. That way you're not dependent on the vagaries of whichever library you're using to talk to the network.
I use Reachability to check for a network connection before performing any requests (which I then perform using Alamofire). Below is a sample function to check for network:
import Reachability
...
func networkIsReachable(shouldShowAlert: Bool) -> Bool {
if let reachability: Reachability = Reachability(), reachability.connection != .none {
return true
}
if shouldShowAlert {
let alertController = UIAlertController(title: "Error", message: "No internet connection.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
present(alertController, animated: true, completion: nil)
}
return false
}
Since I'm using this function all throughout my codebase, I even moved it into an extension so as not to violate DRY principle.
Updating your code to use this function would look like this:
func checkUserCredentials(phoneNumber: String, FBId: String, Token: String) {
guard let networkIsReachable(shouldShowAlert: true) else {
// network is not reachable, and user has been shown an error message
return
}
// now perform network request
// ...
}
I am attempting to sign out of the Firebase API, but I can't seem to figure out how to handle any errors that may occur.
The Firebase pod provides a method for signing out:
FIRAuth.auth()?.signOut()
It is marked with throws, so I have wrapped it in a do/try/catch block in a method to test the signing out process:
do {
try FIRAuth.auth()?.signOut()
} catch (let error) {
print((error as NSError).code)
}
I see that the signOut method is marked with throws in the Firebase pod, but I don't see how it can handle any errors asynchronously. I have tried entering Airplane Mode, which triggers a network error in my code everywhere else that a network request takes place, but with the signOut method, that error isn't caught because I have no completion handler to execute from. All of the other authentication methods from the Firebase pods have a completion handler, in which I am able to handle errors.
Here is the documentation for the signOut method from the Firebase pod:
/** #fn signOut:
#brief Signs out the current user.
#param error Optionally; if an error occurs, upon return contains an NSError object that
describes the problem; is nil otherwise.
#return #YES when the sign out request was successful. #NO otherwise.
#remarks Possible error codes:
- #c FIRAuthErrorCodeKeychainError Indicates an error occurred when accessing the keychain.
The #c NSLocalizedFailureReasonErrorKey field in the #c NSError.userInfo dictionary
will contain more information about the error encountered.
*/
open func signOut() throws
Do you have any suggestions for an appropriate way to handle the signing out of a user when I don't have a completion handler that allows me to check for an error?
You can catch the error like this
do
{
try Auth.auth().signOut()
}
catch let error as NSError
{
print(error.localizedDescription)
}
Edited from Milli's answer to add sending user back to initial page of the app.
// log out
func logout(){
do
{
try Auth.auth().signOut()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let IntroVC = storyboard.instantiateViewController(withIdentifier: "IntroVC") as! introVC
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController = IntroVC
}
catch let error as NSError
{
print(error.localizedDescription)
}
}
An error is highly unlikely to occur but it's never good to assume anything. By the sound of the documentation, it wipes out your keychain which is the only way you'd be able to log back into your firebase application. From trying logging out of my own firebase app I was surprise that 0 errors occured. Here is the original code.
#IBAction func logOutTapped(_ sender: Any) {
let firebaseAuth = FIRAuth.auth()
do {
try firebaseAuth?.signOut()
} catch let signOutError as NSError {
print ("Error signing out: %#", signOutError)
}
if Utility.hasFacebook {
let login = FBSDKLoginManager()
login.logOut()
}
if Utility.hasTwitter {
Twitter.sharedInstance().sessionStore.logOutUserID((Twitter.sharedInstance().sessionStore.session()?.userID)!)
}
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let initialViewController = storyboard.instantiateViewController(withIdentifier: "LoginVC")
self.present(initialViewController, animated: false)
}
Anyways if you really want a completion handler then here's something I tossed up quickly
func logOut(completion:#escaping(_ errorOccured: Bool) -> Void) {
let firebaseAuth = FIRAuth.auth()
do {
try firebaseAuth?.signOut()
} catch let signOutError as NSError {
completion(true)
}
completion(false)
}
When I need to read data from HealthKit this is how my code looks like:
let stepsCount = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
let stepsSampleQuery = HKSampleQuery(sampleType: stepsCount,
predicate: nil,
limit: 100,
sortDescriptors: nil)
{ [unowned self] (query, results, error) in
if let results = results as? [HKQuantitySample] {
self.steps = results
// Update some UI
}
self.activityIndicator.stopAnimating()
}
healthStore?.executeQuery(stepsSampleQuery)
This specific code was extracted from here for demo purpose.
So my question is:
How can I unit test this kind of code ?
I encapsulate this code in a function in a model class that knows nothing about the UI. It works like this:
At the place the you have your
// Update some UI
call a completion closure, that was passed to the function using a parameter.
You call this function from your controller class like this
hkModel.selectSteps() {
[unowned self] (query, results, error) in
// update UI
}
This way you have a clean separation between your query logic in the model class and your UIController code.
Now you can easily write a unit test calling the same method:
func testSteps() {
hkModel.selectSteps() {
[unowned self] (query, results, error) in
// XCTAssert(...)
}
}
The last thing you need is to respect that your test code is called asynchronously:
let stepExpectationEnd = expectationWithDescription("step Query")
hkModel.selectSteps() {
[unowned self] (query, results, error) in
// XCTAssert(...)
stepExpectationEnd.fulfill()
}
waitForExpectationsWithTimeout(10.0) {
(error: NSError?) in
if let error = error {
XCTFail(error.localizedDescription)
}
}
update
Because you asked:
I handle authorization at the test setup. looks like this:
var healthData: HealthDataManager?
override func setUp() {
super.setUp()
healthData = HealthDataManager()
XCTAssert(healthData != nil, "healthDadta must be there")
let authorizationAndAScheduleExpectation = expectationWithDescription("Wait for authorizatiion. Might be manual the first time")
healthData?.authorizeHealthKit({ (success: Bool, error: NSError?) -> Void in
print ("success: \(success) error \(error?.localizedDescription)")
// fails on iPad
XCTAssert(success, "authorization error \(error?.localizedDescription)")
self.healthData?.scheduleAll() {
(success:Bool, error:ErrorType?) -> Void in
XCTAssert(success, "scheduleAll error \(error)")
authorizationAndAScheduleExpectation.fulfill()
}
})
waitForExpectationsWithTimeout(60.0) {
error in
if let error = error {
XCTFail(error.localizedDescription)
}
}
}
The first time you run this code in a simulator, you have to approve authorization manually.
After the first run the tests run without manual intervention.