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
}
Related
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)
}
I have the following method inside my database.swift file:
func GetTestData(arg: Bool, completion: ([Tweet]) -> ()) {
let db = Firestore.firestore()
var tweets = [Tweet]()
db.collection("tweets").getDocuments() {
querySnapshot, error in
if let error = error {
print("unable to retrieve documents \(error.localizedDescription)")
} else{
print("Found documebts")
tweets = querySnapshot!.documents.flatMap({Tweet(dictionary: $0.data())})
}
}
completion(tweets)
}
This method connects to Firestore retrieve data from a given collection, convert it into an array and then passes it back, I call this function with the following (located within my table view controller):
func BlahTest() {
let database = Database()
print("going to get documents")
database.GetTestData(arg: true) { (tweets) in
self.tweets = tweets
self.tableView.reloadData()
}
print("after calling function")
}
The issue I have is when I run this my code is out of sync, and by that I mean print("after calling function") is called before print("Found documebts") which tells me it's not waiting for the async call to Firestore to finish, now I'm new to iOS development so would someone be willing to help me understand how I go about handling this?
Thanks in advance.
You are using closure block in your GetTestData() method. Anything that should be done after execution of this method must be done inside completion:
{
(tweets) in
self.tweets = tweets
self.tableView.reloadData()
// Do rest of stuff here.
}
Following are some resource about implementing async/await in swift like other languages:
1. Async semantics proposal for Swift
2. AwaitKit : The ES8 Async/Await control flow for Swift
3. Concurrency in Swift: One approach
4. Managing async code in Swift
Hope this helps :)
In my app I have a method that makes cloud calls. It has a completion handler. At some point I have a situation when a users makes this call to the cloud and while waiting for the completion, the user might hit log out.
This will remove the controller from the stack, so the completion block will be returned to the controller that is no longer on a stack.
This causes a crash since I do some UI tasks on that completion return.
I did a workaround where, I'm not doing anything with the UI is the controller in no longer on a stack.
However, I'm curious if it's possible to cancel/stop all pending callbacks somehow on logout?
I'm not sure, but I think something is tightly coupled. Try doing:
{ [weak self] () -> Void in
guard let _ = self else { return }
//rest of your code
}
If you get deinitialized then your completioHanlder would just not proceed.
For the granular control over operations' cancellation, you can return a cancellation token out from your function. Call it upon a need to cancel an operation.
Here is an example how it can be achieved:
typealias CancellationToken = () -> Void
func performWithDelay(callback: #escaping () -> Void) -> CancellationToken {
var cancelled = false
// For the sake of example delayed async execution
// used to emulate callback behavior.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if !cancelled {
callback()
}
}
return { cancelled = true }
}
let cancellationToken = performWithDelay {
print("test")
}
cancellationToken()
For the cases where you just need to ensure that within a block execution there are still all necessary prerequisites and conditions met you can use guard:
{ [weak self] in
guard let `self` = self else { return }
// Your code here... You can write a code down there
// without worrying about unwrapping self or
// creating retain cycles.
}
Is there a way to configure Realm so that notifications callbacks (registered with addNotificationBlock) are fired synchronously? In particular, I want this behavior in tests.
Since the callbacks are asynchronous, they can't be used in tests. Therefore, it's necessary to inject a dependency which wraps notification registration in production, while instead injecting a dependency which mimics the behavior in tests.
That's not a great solution though, since it a) requires much more code and b) that code is making assumptions about Realm, such as how to construct a RealmCollectionChange.
If it can't be made to fire synchronously, maybe someone has a suggestion for a better way to test code which relies on RealmCollectionChange?
You can use expectation(description:) and waitForExpectations(timeout:handler:) to test async methods as follows.
func test() {
let q = DispatchQueue(label: "Q")
q.async {
let realm = try! Realm()
try! realm.write {
realm.add(TestObj())
}
}
let e = expectation(description: "Notification fired")
let realm = try! Realm()
let token = realm.addNotificationBlock { (notification, realm) in
print("notification block")
e.fulfill()
}
waitForExpectations(timeout: 2.0, handler: nil)
token.stop()
}
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.