Firebase and Swift 3 Asynchronous Unit Testing - ios

So I've done some research online and the best answers I've found are either outdated or intended for Android. Any help is appreciated!
For another project I did within the last week, I had to write about 2 dozen test cases for a custom Heroku/PostGreSQL backend with Swift 2.3. All I did was create an asyncExpectation variable at the beginning of the test, fulfill the expectation after the completion handler executed, and then wait for it to be fulfilled at the bottom of the test.
Now I'm using Firebase and Swift 3 for a new project. For some reason, nothing gets added to the database when I uncomment the asyncExpectation.fulfill() below. With it commented out, everything works fine. How should I go about testing data storage/retrieval with Firebase and Swift 3?
Edit: I should include that amidst my researching online, I uncovered that I may need to use dispatching, but this seems like a solution for nested completionHandlers, whereas my database append method isn't quite a completionHandler, so I'm at a loss for how to proceed.
class RooMate_v2Tests: XCTestCase {
func testCreateUser() {
let asyncExpectation = expectation(description: "createUserTestOne")
var testSuccess = false
let testUser = (email: "TESTUSER|" + String().randomString(length: 6) + "#test.com", password: String().randomString(length: 6), firstName: "jimmy", lastName: "jenkins")
FIRAuth.auth()?.createUser(withEmail: testUser.email, password: testUser.password) { (user, error) in
if error != nil {
print("Error: \(error.debugDescription)")
} else {
let userProfileInfo = ["firstName" : testUser.firstName,
"lastName" : testUser.lastName,
"email" : testUser.email,
"profilePictureURL" : "N/A"]
let backendRef = FIRDatabase.database().reference()
backendRef.child("users").child("TESTUSER|" + user!.uid).setValue(userProfileInfo)
// testSuccess = true
}
// asyncExpectation.fulfill()
}
waitForExpectations(timeout: 10) { (error) in
XCTAssertTrue(testSuccess)
}
}
}

in my opinion, your problem is that you are checking the success of the setValue in the handler of the wait. As described in the documentation, the handler is called only in case of time out:
A block to be invoked when a call to
-waitForExpectationsWithTimeout:handler: times out or has had all associated expectations fulfilled.
I suggest you to retrieve the value just set and check it, something like this:
let retrievedUserProfile = backendRef.child("users").child("TESTUSER|" + user!.uid)
XCTAssertTrue(retrievedUserProfile == userProfileInfo)
I don't know if this code is correct, it is just to explain my suggestion.
I hope it's helpful!

Related

Serial DispatchQueue is not working as expected

To load some information in my app's view, I need it to finish networking because some methods depend on the result. I looked into serial DispatchQueue and .async methods, but it's not working as expected.
Here is what I tried so far. I defined 3 blocks:
Where I'd get hold of the user's email, if any
The email would be used as input for a method called getData, which reads the database based on user's email address
This block would populate the table view with the data from the database. I've laid it out like this, but I'm getting an error which tells me the second block still executes before we have access to user's email address, i.e. the first block is finished. Any help is appreciated, thanks in advance!
let serialQueue = DispatchQueue(label: "com.queue.serial")
let block1 = DispatchWorkItem {
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if error != nil || user == nil {
print("unable to identify user")
} else {
print(user!.profile?.email ?? "")
self.email = user!.profile?.email ?? ""
print("email is: \(self.email)")
}
}
}
let block2 = DispatchWorkItem{
self.getData(self.email)
}
let block3 = DispatchWorkItem {
DispatchQueue.main.async {
self.todoListTable.reloadData()
}
}
serialQueue.async(execute: block1)
block1.notify(queue: serialQueue, execute: block2)
block2.notify(queue: serialQueue, execute: block3)
Your problem is that you are dispatching asynchronous work inside your work items; GIDSignIn.sharedInstance.restorePreviousSignIn "returns" immediately but fires the completion handler later when the work is actually done. Since it has returned, the dispatch queue considers the work item complete and so moves on to the next item.
The traditional approach is to invoke the second operation from the completion handler of the first (and the third from the second).
Assuming you modified self.getData() to have a completion handler, it would look something like this:
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if error != nil || user == nil {
print("unable to identify user")
} else {
print(user!.profile?.email ?? "")
self.email = user!.profile?.email ?? ""
print("email is: \(self.email)")
self.getData(self.email) {
DispatchQueue.main.async {
self.todoListTable.reloadData()
}
}
}
}
You can see you quickly end up with a "pyramid of doom".
The modern way is to use async/await. This requires your functions to support this approach and imposes a minimum iOS level of iOS 13.
It would look something like
do {
let user = try await GIDSignIn.sharedInstance.restorePreviousSignIn()
if let email = user.profile?.email {
let fetchedData = try await getData(email)
self.data = fetchedData
self.tableView.reloadData()
}
} catch {
print("There was an error \(error)")
}
Ahh. Much simpler to read.

Refresh Token AWS Cognito User Pool, Code Block Not Even Running

I am attempting to refresh a users tokens, and I am doing this in a "child" app that only has access to idToken, accessToken, refreshToken, and the user's information aside for their password. It is sandboxed and cannot communicate to main app.
I have attempted the following code to run initiateAuth
let authParams = [
"REFRESH_TOKEN": user.refreshToken,
"SECRET_HASH": config.getClientSecret()
]
let provider = AWSCognitoIdentityProvider(forKey: config.getClientSecret())
let req = AWSCognitoIdentityProviderInitiateAuthRequest()
req?.authFlow = AWSCognitoIdentityProviderAuthFlowType.refreshToken
req?.clientId = config.getClientId()
req?.authParameters = authParams
provider.initiateAuth(req!).continueWith() {
resp in
print("I'm not running")
if (resp != nil) {
print(resp.error.debugDescription)
} else {
print(resp.result.debugDescription)
}
return nil
}
Shockingly enough, the continueWith block of code doesn't even run at all, and the print statement "I'm not running" is not called. I'm at a loss of what to do, as this seems to be doing what they do in their SDK: https://github.com/aws/aws-sdk-ios/blob/master/AWSCognitoIdentityProvider/AWSCognitoIdentityUser.m#L173
The comment from #behrooziAWS solved this same issue for me.
Specifically, even though the initializer for AWSCognitoIdentityProvider(forKey:) is documented in XCode's quick help as:
+ (nonnull instancetype)CognitoIdentityProviderForKey:(nonnull NSString *)key;
it can (and it will) return nil if the CognitoIdentityProvider for the provided key is not found. One case this happens is when AWSCognitoIdentityProvider.register(with:forKey:) was never called for the provided key.
It's even more confusing when if(provider != nil) produces a compiler warning that it will always be true. I had to use the following code snippet to get things working without warnings:
let provider: AWSCognitoIdentityProvider? = AWSCognitoIdentityProvider(
forKey: config.getClientSecret())
guard provider != nil else {
fatalError("AWSCognitoIdentityProvider not registered!")
}
P.S. I hope someone with more experience in Swift+ObjC can comment on why the initializer shows up or is translated as nonnull instancetype even though the Objective C source code only has instancetype.
Edit: The reason why this initializer (and pretty much everything else in the AWSCognitoIdentityProvider/AWSCognitoIdentityUser.h file) is annotated with nonnull automatically is because of the NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END macros. In the case of CognitoIdentityProviderForKey, this non-null assumption should not be valid.

In Swift-iOS How I catch User Authentication erros in AWS Cognito?

I'm trying aws cognito user pool and got stacked in the user sign up process. I already configured my user pool and are executing the sign-up method, but I can find a way to get the error code returned by aws services. Here my user pool instantiation, that is working fine:
let poolConfig = AWSCognitoIdentityUserPoolConfiguration(
clientId: userPool_clientId,
clientSecret: userPool_secret,
poolId: userPool_id)
AWSCognitoIdentityUserPool.registerCognitoIdentityUserPool(with: poolConfig,
forKey: userPoll_App)
userPool = AWSCognitoIdentityUserPool(forKey: userPoll_App)
Then, in my view controller I have a Button whit a #IBAction with this:
if userPool != nil {
let attName = AWSCognitoIdentityUserAttributeType()!
attName.name = "name"
attName.value = userNome
let attLast = AWSCognitoIdentityUserAttributeType()!
attLast.name = "family name"
attLast.value = userSobrenome
let attEmail = AWSCognitoIdentityUserAttributeType()!
attEmail.name = "email"
attEmail.value = userEmail
var result:Bool = false
userPool!.signUp(userNome,
password: userPwd,
userAttributes: [attName, attLast, attEmail],
validationData: nil).continue({(task:AWSTask!) in
if (task.error != nil) {
print (task.error!)
result = false
} else {
result = true
}
return nil
})
After that code, I test the result to see if it is true or false and take the appropriate action. But...
I'm having different errors in this process and I need to evaluate this errors in development time. For example, the first error that I got was because I misconfigured the AWS region. Ok! Game on!! But the second error was because the password informed by the user did not passed the validation of the pool. In this case, I want to know the error was because the validation process and inform the user to take the appropriate action. I do not want to have this logic in the iOS app. The task.error object just give a localized description property and it is not very helpful.
By the way: I'm using Swift 3.2, iOS 10.2, aws-ios-sdk2 and Xcode 8.
I would like to expand on behrooziAWS's answer.
In Swift 4 you can match the error code with enums like AWSCognitoIdentityProviderErrorType.TheErrorType.rawValue.
Here's a tip for searching your error type, just type "AWSErrorType" and Xcode's autocomplete would show all the enums and then you can look through them.
Here's a code I use.
AWSobject.AWSfunction().continueWith { task -> Any? in
if let err = task.error as NSError? {
switch err.code {
case: AWSCognitoIdentityProviderErrorType.userNotFound.rawValue:
// Handle case
default:
// Handle all other cases here
return nil
}
// Do stuff on success!
}
task.error.code will contain a code you can compare to values in this enum. Look here for the particular error codes that can be returned by SignUp.

Testable Asynchronous Design Pattern in Swift

I'm learning Test Driven Development in Swift. I hit a wall when I realized the delegate pattern I regularly use for asynchronous requests is difficult to test. I've learned that if something's difficult to test, the design pattern behind the implementation could probably be better. This is confusing me because I think the delegate pattern I'm using is common and I'm wondering how others have dealt with this issue.
The pattern:
I wrote a service, which executes an asynchronous request in a static function which takes a delegate instance. The delegate instance conforms to a protocol which requires implementation of a success and failure method. I've contrived an example which hits Google.com. Please ignore the Type safety issues in this example. The actual code I'm running to hit an endpoint and parse JSON is safer. I just wanted to come up with a very small snippet of code to depict the issue that's causing difficulty while testing:
protocol GoogleServiceDelegate {
func gotGoogle(str: String);
func gotError(str: String);
}
struct GoogleService {
static func getGoogle(delegate: GoogleServiceDelegate) {
let url: NSURL! = NSURL(string: "http://google.com")
NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
if let data = data {
let str: NSString! = NSString(data: data, encoding: NSUTF8StringEncoding)
delegate.gotGoogle(str as String)
} else {
delegate.gotError("\(error)")
}
}
}
}
Here's the test which illustrates the problem:
class AsyncTestingTests: XCTestCase {
func testExample() {
let responseExpectation = expectationWithDescription("Got google response!")
struct GoogleDelegate: GoogleServiceDelegate {
func gotGoogle(str: String) {
// expectations about response
responseExpectation.fulfill()
}
func gotError(str: String) {
// expectations about error
responseExpectation.fulfill()
}
}
let myGoogleServiceDelegate = GoogleDelegate()
GoogleService.getGoogle(myGoogleServiceDelegate)
waitForExpectationsWithTimeout(5) { _ in
print("Never got a response from Google :(")
}
}
}
The problem arises at the two .fulfill() lines. I get the following error from Xcode:
Struct declaration cannot close over value 'responseExpectation' defined in outer scope
I understand the error, but am unsure what to adjust... Is there a workaround for this which I can use in the test, or is there a better (easily testable) pattern for asynchronous callbacks than what I am attempting? If you know of a better testable solution, would you mind taking the time to write down an example?
Yes, you can not close over variables defined outside of struct, to workaround, we need to use closures/functions and pass it to the struct. Methods in struct can invoke it when they receive the response.
func testExample() {
let responseExpectation = expectationWithDescription("Got google response!")
//Let a function capture the fulfilling of the expectation
func fullFillExpectation(){
responseExpectation.fullFill()
}
struct GoogleDelegate: GoogleServiceDelegate {
var fullFiller : (()->Void)!
func gotGoogle(str: String) {
// expectations about response via invoke the closure
fullFiller()
}
func gotError(str: String) {
// expectations about error - invoke the closure
fullFiller()
}
}
//Create the delegate with full filler function.
let myGoogleServiceDelegate = GoogleDelegate(fullFiller: fullFillExpectation)
GoogleService.getGoogle(myGoogleServiceDelegate)
waitForExpectationsWithTimeout(5) { _ in
print("Never got a response from Google :(")
}
}
}
PS: I could not test this, please test and let me know.

Parse PFCloud.callInBackground completion block never called i.e no response received

I have defined a function in the Parse Cloud Code called "relatedWords". When I try call this function in my iOS app, the completion block/closure is never called i.e no response is received.
I've tested the function in the Parse API Console and it is working fine there, so I know it's not an issue with the cloud code.
Any ideas on what the issue is?
My swift code:
func fetchRelatedKeyWordsForWord(word: String)
{
PFCloud.callFunctionInBackground("relatedWords", withParameters: ["hashtag": word]) { (response, error) -> Void in
//This is never called
print(response)
print(error)
}
}
Snippet of the cloud code:
Parse.Cloud.define("relatedWords", function(request, response) {
Parse.Cloud.useMasterKey();
var hashtag = request.params.hashtag;
...
...
//Run a query
var query = new Parse.Query(parseClassName);
query.find({
success: function(results) {
if (results.length != 0) {
console.log("Found Objects! Returning Objects");
response.success(results);
return;
}
Edit:
I figured out the problem. It was silly mistake by me. The reason the cloud code was not getting called is that I had not setup parse in my ApplicationDidFinishLaunching i.e I did not call Parse.setApplicationId("...", clientKey: "...")
I figured out the problem. It was silly mistake by me. The reason the cloud code was not getting called is that I had not setup parse in my ApplicationDidFinishLaunching i.e I did not call Parse.setApplicationId("...", clientKey: "...")
I figured out the problem.
you can use other server, other vise pay money on parse and solve the problem.

Resources