How to wait until get the response from component under test that use Alamofire? - Xcode - swift

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!

Related

Swift: Recursive async func with completion handler that does not get called

I need to fetch large amounts of data from an endpoint in an async way. The API endpoint serves a predefined amount of data at a time. After the first request I must check to see if I get a "next" url from the response and visit that link in order to continue the download. This recursive behaviour continues until all available data has been served, in other words paging functionality (HAL links). At this point I have implemented a func that download recursively, however: problem is that the final completion handler does not seem to get called.
Demo code: The ThingsApi is a class that encapsulates the actual API call. The important thing is that this class has an initial url and during recursion will get specific url's to visit asynchronously. I call the downloadThings() func and need to get notified when it is finished. It works if I leave recursion out of the equation. But when recursion is in play then nothing!
I have created a simplified version of the code that illustrate the logic and can be pasted directly into the Playground. The currentPage and pages var's are just there to demo the flow. The last print() statement does not get called. Leave the currentPage += 1 to experience the problem and set currentPage += 6 to avoid recursion. Clearly I am missing out of some fundamental concept here. Anyone?
import UIKit
let pages = 5
var currentPage = 0
class ThingsApi {
var url: URL?
var next: URL?
init(from url: URL) {
self.url = url
}
init() {
self.url = URL(string: "https://whatever.org")
}
func get(completion: #escaping (Data?, HTTPURLResponse?, Error?) -> Void) {
// *** Greatly simplified
// Essentially: use URLSession.shared.dataTask and download data async.
// When done, call the completion handler.
// Simulate that the download will take 1 second.
sleep(1)
completion(nil, nil, nil)
}
}
func downloadThings(url: URL? = nil, completion: #escaping (Bool, Error?, String?) -> Void) {
var thingsApi: ThingsApi
if let url = url {
// The ThingsApi will use the next url (retrieved from previous call).
thingsApi = ThingsApi(from: url)
} else {
// The ThingsApi will use the default url.
thingsApi = ThingsApi()
}
thingsApi.get(completion: { (data, response, error) in
if let error = error {
completion(false, error, "We have nothing")
} else {
// *** Greatly simplified
// Parse the data and save to db.
// Simulate that the thingsApi.next will have a value 5 times.
currentPage += 1
if currentPage <= pages {
thingsApi.next = URL(string: "https://whatever.org?page=\(currentPage)")
}
if let next = thingsApi.next {
// Continue downloading things recursivly.
downloadThings(url: next) { (success, error, feedback) in
guard success else {
completion(false, error, "failed")
return
}
}
} else {
print("We are done")
completion(true, nil, "done")
print("I am sure of it")
}
}
})
}
downloadThings { (success, error, feedback) in
guard success else {
print("downloadThings() failed")
return
}
// THIS DOES NOT GET EXECUTED!
print("All your things have been downloaded")
}
It seems like this is simply a case of "you forgot to call it yourself" :)
In this if statement right here:
if let next = thingsApi.next {
// Continue downloading things recursivly.
downloadThings(url: next) { (success, error, feedback) in
guard success else {
completion(false, error, "failed")
return
}
}
} else {
print("We are done")
completion(true, nil, "done")
print("I am sure of it")
}
Think about what happens on the outermost call to downloadThings, and execution goes into the if branch, and the download is successful. completion is never called!
You should call completion after the guard statement!

Swift service call, handling response

I am writing the iOS application using swift 4.2. I am making a service call to logout user.
I need to know where to use main thread (DispatchQueue.main.async).
Here is my code:
private func handleLogoutCellTap() {
logoutUseCase?.logout() { [weak self] (result) in
guard let self = self else { return }
switch result {
case let (.success(didLogout)):
didLogout ? self.handleSuccessfullLogout() : self.handleLogoutError(with: nil)
case let (.failure(error)):
self.handleLogoutError(with: error)
}
}
}
logoutUseCase?.logout() makes a service call and returns #escaping completion. Should I use DispatchQueue.main.async on this whole handleLogoutCellTap() function or just in a handling segment?
Move the control to main thread wherever you're updating the UI after receiving the response of logout.
If handleSuccessfullLogout() and handleLogoutError(with:) methods perform any UI operation, you can embed the whole switch statement in DispatchQueue.main.async, i,e.
private func handleLogoutCellTap() {
logoutUseCase?.logout() { [weak self] (result) in
guard let self = self else { return }
DispatchQueue.main.async { //here.....
switch result {
//rest of the code....
}
}
}
}

Firebase Functions in swift does not return anything if there is no Internet

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
// ...
}

Testing text of an animated label that appears and disappears

I am struggling to test the appearance of a label(toastLabel) which I have that animates briefly into view when someone enters the wrong email.
private func registerNewUser(email: String, password: String, confirmationPassword: String) {
if password == confirmationPassword {
firebaseData.createUser(email: email, password: password, completion: { (error, _ ) in
if let error = error {
self.showToast(in: self.view, with: error.localizedDescription)
} else {
self.showToast(in: self.view, with: "Registered succesfully")
self.signInUser(email: email, password: password)
}
})
} else {
//raise password mismatch error
print("password mismatch error")
}
}
func showToast(in toastSuperView: UIView, with text: String) {
let toastLabel = ToastLabel()
toastLabel.text = text
toastSuperView.addSubview(toastLabel)
layoutToastLabel(toastLabel)
animateToastLabel(toastLabel)
}
private func layoutToastLabel(_ toastLabel: ToastLabel) {
toastLabel.centerYToSuperview()
toastLabel.pinToSuperview(edges: [.left, .right])
}
private func animateToastLabel(_ toastLabel: ToastLabel) {
UIView.animate(withDuration: 2.5, delay: 0, options: .curveEaseOut, animations: {
toastLabel.alpha = 0.0
}, completion: { _ in
toastLabel.removeFromSuperview()
})
}
I just want to test that the error text received back from firebase appears after the user enters an email that has already been taken.
func testRegisteringWithUsedEmailDisplaysFirebaseError() {
let email = registeredEmail
let password = "password"
welcomeScreenHelper.register(email: email,
password: password,
confirmationPassword: password,
completion: {
let firebaseErrorMessage = "The email address is already in use by another account."
XCTAssert(self.app.staticTexts[firebaseErrorMessage].exists)
})
}
func register(email: String, password: String, confirmationPassword: String, completion: (() -> Void)? = nil) {
let emailTextField = app.textFields[AccesID.emailTextField]
let passwordTextField = app.secureTextFields[AccesID.passwordTextField]
let confirmPasswordTextField = app.secureTextFields[AccesID.confirmPasswordTextField]
let registerButton = app.buttons[AccesID.registerButton]
emailTextField.tap()
emailTextField.typeText(email)
passwordTextField.tap()
passwordTextField.typeText(password)
registerButton.tap()
confirmPasswordTextField.tap()
confirmPasswordTextField.typeText(confirmationPassword)
registerButton.tap()
completion?()
}
when I use other tools such as expectation and XCTWaiter the tests still don't pass despite the text and label definitely appearing. I have never had to do a test like this so I'm not sure where I may be going wrong, whether I have to do something different to test an animated view or something.
Update1:
So I can see after a bit more playing that when i tap the registerButton the toast appears as it should but the test doesn't continue until it has disappeared again. I find this odd as it's not strictly attached to the registerButton being its own view.
update2:
I have update my test as follows:
func testRegisteringWithUsedEmailDisplaysFirebaseError() {
welcomeScreenHelper.register(email: registeredEmail,
password: password,
confirmationPassword: password,
completion: {
let firebaseErrorMessage = "The email address is already in use by another account."
let text = self.app.staticTexts[firebaseErrorMessage]
let exists = NSPredicate(format: "exists == true")
self.expectation(for: exists, evaluatedWith: text, handler: nil)
self.waitForExpectations(timeout: 10, handler: nil)
XCTAssert(self.app.staticTexts[firebaseErrorMessage].exists)
})
}
with the addition of:
override func setUp() {
app.launch()
UIView.setAnimationsEnabled(false)
super.setUp()
}
override func tearDown() {
if let email = createdUserEmail {
firebaseHelper.removeUser(with: email)
}
UIView.setAnimationsEnabled(true)
super.tearDown()
}
But so far no luck. I can still see that in func register once the register button is tapped the toast shows and the next line isn't called until the toastLabel has finished animating.
There are several things you need to solve in such kind of test:
If the code you are testing is using DispatchQueue.async you should use XCTestCase.expectation
If the code you are testing has UIView.animate in it (I see there is one in your example) do UIView.setAnimationsEnabled(false) before the test and enable it back after the test finishes so expectation won't wait for animation to complete. You can do it in XCTestCase.setUp and XCTestCase.tearDown methods.
If the code you are testing has dependencies like services that are doing async calls (I assume firebaseData is) you should either inject their mocks/stubs that will behave synchronously or use XCTestCase.expectation and pray for API/network be OK while the tests are run.
So using XCTestCase.expectation + UIView.setAnimationsEnabled(false) should work for you. Just XCTestCase.expectation with high enough timeout should work too.
EDIT 1:
Correct way to use expectation:
func test() {
let exp = expectation(description: "completion called")
someAsyncMethodWithCompletion() {
exp.fulfill()
}
waitForExpectations(timeout: 1) { _ in }
// assert here
}
So your test method should be:
func testRegisteringWithUsedEmailDisplaysFirebaseError() {
let exp = expectation(description: "completion called")
welcomeScreenHelper.register(email: registeredEmail,
password: password,
confirmationPassword: password,
completion: { exp.fulfill() })
self.waitForExpectations(timeout: 10, handler: nil)
let firebaseErrorMessage = "The email address is already in use by another account."
XCTAssert(self.app.staticTexts[firebaseErrorMessage].exists)
}

Unit Test HKSampleQuery in Swift

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.