XCTest and using mock objects as notification receivers - ios

In XCTest with swift you can define mock objects in the test function you need it in. Like so
func testFunction(){
class mockClass: AnyObject{
func aFunction(){
}
}
}
I'm trying to use these mock objects to test that another function sends out the correct notification given a certain condition (in my case that the success notification is broadcast with a 204 status code to an object.
The problem I am having is that i get an "Unrecognised selector" runtime error even though the deletedSuccess() function is clearly there/
Heres some code dump
func testDelete(){
let expectation = expectationWithDescription("Post was deleted")
class MockReciever : AnyObject {
func deleteSuccess(){
println("delete successfull")
expectation.fulfill()
}
}
let mockReciever = MockReciever()
NSNotificationCenter.defaultCenter().addObserver(mockReciever, selector: "deleteSuccess", name: PostDeletedNotification, object: post)
let response = NSHTTPURLResponse(URL: NSURL(), statusCode: 204, HTTPVersion: nil, headerFields: nil)
let request = NSURLRequest()
post.deleteCompletion(request, response: response, data: nil, error: nil)
waitForExpectationsWithTimeout(30, handler: { (error) -> Void in
if error != nil{
XCTFail("Did not recieve success notification")
} else {
XCTAssertTrue(true, "post deleted successfully")
}
NSNotificationCenter.defaultCenter().removeObserver(mockReciever)
})
}
Is there some sort of problem with using mock objects and selectors like this that I don't know about?

You don't have to implement mock objects to test notifications. There is a -[XCTestCase expectationForNotification:object:handler:] method.
And here's an answer on how to receive notifications from NSNotificationCenter in Swift classes that do not inherit from NSObject. Technically it's a duplicate question.

Related

Write simple test for api call

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)
}

Swift 3.0 + iOS 10 + Singleton sharedInstance - Update the UI after data array changes (the Angular Way)

Soooo.... I'm a JS dev and I'm building an app in Swift 3.0 for iOS 10+ and I want to interact with data much in the same way I do in Angular 1.6+...
Here is my situation:
I have a Singleton sharedInstance that house session data for a webRTC session. One of the properties of the sharedInstance holds an array of UInt and I need to update a UI element (IBOutlet) to show the user who the available opponents are they can call at any given moment.
In angular I would just update the model and the view/UI would change automatically... boom bang... done...
I am looking to create the same behavior in Swift 3.0 so here goes some code:
class Singleton {
static let sharedInstance = Singleton()
var session = (
peers: [UInt]()
)
private init() { }
}
Here is the controller:
class ViewController: UIViewController {
#IBOutlet weak var UIPeerList: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
UIPeerList.text = String(describing: Singleton.sharedInstance.session.peers)
self.updatePeerList(room: roomName, completion: {(oUser: AnyObject) -> Void in
QBRequest.dialogs(
for: QBResponsePage(limit: 100, skip: 0),
extendedRequest: [roomName : "name"],
successBlock: {(
response: QBResponse,
dialogs: [QBChatDialog]?,
dialogsUsersIDs: Set<NSNumber>?,
page: QBResponsePage?
) -> Void in
print("<------------------------------<<< Successfully found chat dialog - " + roomName)
}, errorBlock: { (response: QBResponse) -> Void in
print("<------------------------------<<< Handle Error finding chat dialog - " + roomName)
let chatDialog: QBChatDialog = QBChatDialog(dialogID: nil, type: QBChatDialogType.group)
//chatDialog.occupantIDs = []
//chatDialog.occupantIDs?.append(NSNumber(value: DataModel.sharedInstance.qbLoginParams.id))
//chatDialog.occupantIDs?.append(12186)
chatDialog.setValue(roomName, forKey: "Name")
QBRequest.createDialog(chatDialog, successBlock: {(response: QBResponse?, createdDialog: QBChatDialog?) in
print("<------------------------------<<< Success creating chat dialog")
print(response ?? "No Response")
print(createdDialog ?? "No Created Dialog")
}, errorBlock: {(response: QBResponse!) in
print("<------------------------------<<< Error creating chat dialog")
print(response ?? "No Response")
})
}
)
})
So above in the controller I set the UI element to the datasource right after the viewDidLoad is triggered (I know this is not exactly what's going on currently.) Now whenever I change the Singleton.sharedInstance.session.peers data model like for instance here in this ASYNC API request I want the UI to update:
func updatePeerList( room: String, completion: #escaping (_ response: AnyObject) -> ()) {
QBRequest.users(
withTags: [room],
page: QBGeneralResponsePage(currentPage: 1, perPage: 10),
successBlock: {( response: QBResponse, page: QBGeneralResponsePage?, users: [QBUUser]? ) -> Void in
guard users != nil else { return }
print("<------------------------------<<< Success getting users with room tag - "+room)
DataModel.sharedInstance.sessionInfo.peers.removeAll()
for object in users! {
DataModel.sharedInstance.sessionInfo.peers.append(object.id)
}
DispatchQueue.main.async { completion(response) }
}, errorBlock: {(response: QBResponse!) in
print("<------------------------------<<< Error getting users with room tag - "+room)
print(response)
DispatchQueue.main.async { completion(response) }
}
)
}
How do I achieve such a feat?
NOTE: I believe this is possible as I looked into the UISwitch method however that didn't exactly work as I wanted to..
One solution that came to my mind is using delegation. There may be more elegant solutions though.
To use delegation, you could go through the following steps:
1- Create a protocol that have one method update(), and let's name that protocol UpdateUIDelegate.
2- Make your ViewController implement that protocol, and implement update() method in the way you want.
3- Set your ViewController object as a property in the Singleton class.
4- Make all changes that affect Singleton.sharedInstance.session.peers happen through a method inside Singleton, and make that method calls your delegate's update method in its end.

Background API POST that handle failure

In my iOS app users complete transactions which I need to post back to the server. I've created a function to do this:
static let configurationParam = NSURLSessionConfiguration.defaultSessionConfiguration()
static var manager = Alamofire.Manager(configuration: configurationParam)
func postItemToServer(itemToPost:DemoItem) {
let webServiceCallUrl = "..."
var itemApiModel:[String: AnyObject] = [
"ItemId": 123,
"ItemName": itemToPost.Name!,
//...
]
ApiManager.manager.request(.POST, webServiceCallUrl, parameters: itemApiModel, encoding: .JSON)
.validate()
.responseJSON { response in
switch response.result {
case .Success:
print("post success")
case .Failure:
print("SERVER RESPONSE: \(response.response?.statusCode)")
}
}
}
Currently I call this once a transaction is complete:
//...
if(transactionCompleted!) {
let apiManager = ApiManager()
apiManager.postItemToServer(self.item)
self.senderViewController!.performSegueWithIdentifier("TransactionCompletedSegue", sender: self)
}
//...
Where DemoItem is a CoreData object.
This all works as expected. However I need the ability to retry the POST request if it fails. For example if the network connection is down at the point of trying post to the server I need to automatically post the data once it becomes active again - at which point there may be several DemoItem's which need to be synced.
I'm new to Swift. In a similar Xamarin app I had a status column in my SQLite database which I set to 'AwaitingSync'. I then had an async timer that ran every 30 seconds, queried the DB for any items which had status='AwaitingSync' and then tried to post them if they existed. If it succeed it updated the status in the DB. I could implement something along the same lines here - but I was never really happy with that implementation as I had a DB query every 30 seconds even if nothing had changed.
Finally, it needs to be still work if the app is terminated. For example any items which weren't synced before the app is killed should sync once the app is resumed. What's the best way to approach this?
Edit
Based on Tom's answer I've created the following:
class SyncHelper {
let serialQueue = dispatch_queue_create("com.mycompany.syncqueue", DISPATCH_QUEUE_SERIAL)
let managedContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
func StartSync() {
//Run on serial queue so it can't be called twice at once
dispatch_async(serialQueue, {
//See if there are any items pending to sync
if let itemsToSync = self.GetItemsToSync() {
//Sync all pending items
for itemToSync in itemsToSync {
self.SyncItemToServer(itemToSync)
}
}
})
}
private func GetItemsToSync() -> [DemoItem]? {
var result:[DemoItem]?
do {
let fetchRequest = NSFetchRequest(entityName: "DemoItem")
fetchRequest.predicate = NSPredicate(format: "awaitingSync = true", argumentArray: nil)
result = try managedContext.executeFetchRequest(fetchRequest) as? [DemoItem]
} catch {
//Handle error...
}
return result
}
private func SyncItemToServer(itemToSync:DemoItem) {
let apiManager = ApiManager()
//Try to post to the server
apiManager.postItemToServer(itemToSync:DemoItem, completionHandler: { (error) -> Void in
if let _ = error {
//An error has occurred - nothing need to happen as it will be picked up when the network is restored
print("Sync failed")
} else {
print("Sync success")
itemToSync.awaitingSync = false
do {
try self.managedContext.save()
} catch {
//Handle error...
}
}
})
}
}
I then call this when ever a transaction is completed:
//...
if(transactionCompleted!) {
let syncHelper = SyncHelper()
syncHelper.StartSync()
}
//...
And then finally I've used Reachability.swift to start the sync every time the network connection resumes:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var reachability:Reachability?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//...
//Setup the sync for when the network connection resumes
do {
reachability = try Reachability.reachabilityForInternetConnection()
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "reachabilityChanged:",
name: ReachabilityChangedNotification,
object: reachability)
try reachability!.startNotifier()
} catch {
print("Unable to create Reachability")
}
return true
}
func reachabilityChanged(note: NSNotification) {
let reachability = note.object as! Reachability
if reachability.isReachable() {
print("Network reachable")
let syncHelper = SyncHelper()
syncHelper.StartSync()
} else {
print("Not reachable")
}
}
}
This all seems to be working. Is this approach ok and have I missed anything which would improve it? The only gap I can see is if the network connectivity is active however the server throws an error for some reason - I guess I could then add a button for the user to retry any pending items.
Firstly, if your concern is whether the network connection is working, you shouldn't be polling at intervals. You should be using iOS's network reachability API to get notified when the network status changes. Apple provides a simple implementation of this and there are numerous alternative implementations online.
Since a sync status value should be a boolean flag, it's not as if a fetch request is a heavy-duty operation, especially if you use reachability. Not only should the fetch request be fast, you can update the flag after the fact in a single step-- use NSBatchUpdateRequest to set the flag to false on every instance you just sent to the server.
If you want to get the sync status out of the persistent store (not a bad idea since it's metadata), you'll need to maintain your own list of unsynced objects. The best way to do this is by tracking the objectID of the managed objects awaiting sync. That would be something like:
Get the objectID of a newly changed managed object
Convert that to an NSURL using NSManagedObjectID's URIRepresentation() method.
Put the NSURL on a list that you save somewhere, so it'll persist.
You can save the list in a file, in user defaults, or in the persistent store's own metadata.
When it's time to sync, you'd do something like:
Get an NSURL from your list
Convert that into an NSManagedObjectID using managedObjectIDForURIRepresentation(url:NSURL) (which is on NSPersistentStoreCoordinator)
Get the managed object for that ID objectWithID: on NSManagedObjectContext.
Sync that object's data.
Then on a successful sync, remove entries from the list.

No array sent from parent app to Watch app

I am trying to receive an array of objects (that are retrieved from Parse within the app) from a parent application to be displayed in the watch application. I have been trying a few different things but with no success.
Here is my code in the extension:
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
var parkPassed = context as! String
openParentAppWithPark(parkPassed)
}
private func openParentAppWithPark(park: String) {
WKInterfaceController.openParentApplication(["request": park], reply: { (reply, error) -> Void in
println(reply)
})
}
And the code in the parent app:
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
println("Test")
if let userInfo = userInfo, request = userInfo["request"] as? NSArray {
if request == "Park 1" {
DataManager.sharedInstance.loadRides("Park 1")
} else if request == "Park 2" {
DataManager.sharedInstance.loadRides("Park 2")
} else if request == "Park 3" {
DataManager.sharedInstance.loadRides("Park 3")
} else {
DataManager.sharedInstance.loadRides("Park 4")
}
let rides = DataManager.sharedInstance.rideArray
println("Rides: \(rides)")
reply(["rideData": rides])
return
}
reply([:])
}
The println I have always returns nil the first time I try to load, and then [:] every other time. I assume this is because the request is timing out before the app has time to load the data from Parse? Also, the println that is supposed to print "Test" is never called.
In the extension, you're passing a String (park) to the parent application via the request key, but in the parent application, you're testing whether userInfo["request"] is an NSArray or not. You should be testing for a String, as in:
if let userInfo = userInfo, request = userInfo["request"] as? String {
First add a background task assertion to the openParentCall, you can find more context on that here: Background Task Watchkit
let backgroundTask = application.beginBackgroundTaskWithExpirationHandler { NSLog("TIME UP")}
///do code
reply(callback)
//
application.endBackgroundTask(backgroundId)
Now for the actual handleWatchKitExtensionRequest call I would change the first line to
if let request = userInfo["request"] as? String {
Now for the println("Test") not printing to console if you don't attach to process with the parentApplication then the println will not log out.
If the ride data is returning empty then I would inspect this function:
DataManager.sharedInstance.loadRides(ride: String)
make sure it is actually returning the correct data you need. Attach to process and place a breakpoint on each case and check that one of the cases is being called and also jump into the loadRides function to make sure it is coming back out from it. As a side note, the information you send back in the reply block has to be a property list or the reply block will always fail.

Swift Progress View Value Passed Around Functions

I am needing to implement a progress bar that takes into account a couple of factors.
I have three different classes, my ViewController, a Networking class to handle the network calls and a dataManager class to handle all the db operations.
Now my progressView lives in my viewcontroller and I am looking at a way of updating it as each of the different operations are performed in the other classes.
I am using Alamofire so I know I can use .progress{} to catch the value of the JSON progress but that would also mean exposing the ViewController to the Networking class, which I assume is bad practice?
I think this should be achieved using completion handlers but as I have already setup another thread for handling the JSON / DB operation I'm not wanting to over complicate it anymore than I need to
Networking:
func makeGetRequest(url : String, params : [String : String]?, completionHandler: (responseObject: JSON?, error: NSError?) -> ()) -> Request? {
return Alamofire.request(.GET, url, parameters: params, encoding: .URL)
.progress { _, _, _ in
//bad practice?
progressView.setProgress(request.progress.fractionCompleted, animated: true)
}
.responseJSON { request, response, data, error in completionHandler(
responseObject:
{
let json = JSON(data!)
if let anError = error
{
println(error)
}
else if let data: AnyObject = data
{
let json = JSON(data)
}
return json
}(),
error: error
)
}
}
ViewController:
dataManager.loadData({(finished: Bool, error:NSError?) -> Void in
if let errorMessage = error{
self.syncProgress.setProgress(0, animated: true)
let alertController = UIAlertController(title: "Network Error", message:
errorMessage.localizedDescription, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}
if finished{
for i in 0..<100 {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {
sleep(1)
dispatch_async(dispatch_get_main_queue(), {
self.counter++
return
})
})
}
}
})
As you can see I am waiting on the finished boolean in the datamanger class to be set before updating the progress bar. The thing is, dataManager makes a call to networking and performs a bunch of other stuff before it finishes, it would be handy to update the progress bar along the way but I'm not sure of the best approach?
DataManager:
func loadData(completion: (finished: Bool, error: NSError?) -> Void) {
var jsonError: NSError?
networking.makeGetRequest(jobsUrl, params: nil) { json, networkError in
//....
}
I'm not too familiar with swift so I can't give you a code example but the way I would do this is create a protocol on your Networking class NetworkingDelegate and implement that protocol in your ViewController. The protocol method would be something like (in objective-c) NetworkingRequestDidUpdatProgress:(float progress)
This is assuming your ViewController calls Networking.makeGetRequest. If it's another class, you would implement the delegate in that class, or you could bubble up the delegate calls to your ViewController through the DataManager class.

Resources