Trying to unit test a network call and getting the following error:
API violation - multiple calls made to -[XCTestExpectation fulfill]
If written this style of testing before, but this one seems to be creating the error and I can't seem to figure out why.
func testAccessKeys() {
let expected = expectation(description: "Run the Access request")
sut.request(.Access, data: nil) { finished, response in
if response != nil && finished == true {
if let json = response as? [String:Any] {
if json["id"] as? String != nil, json["secret"] as? String != nil {
XCTAssertNotNil(json)
expected.fulfill()
} else {
XCTFail("Access response was not in correct format")
expected.fulfill()
}
} else {
XCTFail("Access request was not a dictionary")
expected.fulfill()
}
} else {
XCTFail("Access response was nil")
expected.fulfill()
}
}
waitForExpectations(timeout: 3) { error in
if let error = error {
XCTFail("Access request failure: \(error.localizedDescription)")
}
}
}
UPDATE
Simplifying the call solved the problem:
func testAccessKeys() {
let expected = expectation(description: "Run the Access request")
sut.request(.Access, data: nil) { finished, response in
if response != nil && finished == true {
if let json = response as? [String:Any] {
if json["id"] as? String != nil, json["secret"] as? String != nil {
print("ID: \(String(describing: json["id"]))")
print("Secret: \(String(describing: json["secret"]))")
expected.fulfill()
} else {
XCTFail("Access response was not in correct format")
}
} else {
XCTFail("Access request was not a dictionary")
}
} else {
XCTFail("Access response was nil")
}
}
waitForExpectations(timeout: 3, handler: nil)
}
I had the similar problem. I resolved it by making sure that the expectation.fulfill() is called only once. Also Make sure that your expectation has different description if you have more than one expectation. Eg:self.expectation(description: "expected description")
Calling it more than once leads to crash.
In my case I had a test case dependent on another test case. There the expectation was called more than once. I resolved it by making it as single one
You could have posted your UPDATE as answer!
Related
I'm using RealTime DataBase for my application, it has security rules:
{
"rules": {
".read": "auth.uid != null",
".write": "auth.uid != null"
}
}
This is the code snippet where the data is getting:
func firstLaunchCataloguePartsFetchData(
result : #escaping((Result<[CatalogueParts], Error>) -> Void)
) {
if ConnectionManager.shared.isConnected {
let baseUrl = Constants.baseUrl + DirectoryType.catalogueParts.addJsonAbbreviation
guard let url = URL(string: baseUrl ) else { return }
URLSession.shared.dataTask(with: url) {data, response, error in
if let error = error {
result(.failure(error))
} else if let data = data,
let jsonString = String(data: data, encoding: .utf8),
let downloadedMarks = Mapper<CatalogueParts>().mapArray(JSONString: jsonString) {
result(.success(downloadedMarks))
} else {
result(.success([]))
}
}
.resume()
} else {
let error = NSError(domain: "Connection error detected", code: 911, userInfo: nil)
result(.failure(error))
}
}
Here I am using the received data to draw a table view:
func getFirstLaunchCatalogueParts() {
ShopManager.shared.firstLaunchCataloguePartsFetchData { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let catalogueParts):
self.catalogueParts = catalogueParts
DispatchQueue.main.async {
self.tableView.reloadData()
}
case .failure(let error as NSError):
self.showErrorAlert(error: error, withAction: nil)
}
}
}
Even if I'm authorized, passed phone authorization (Auth.auth().currentUser?.uid has somevalue) - i can't display the tableView, received data is nil
But!
This works great, the table is rendered, everything works if the rules are made like this:
{
"rules": {
".read": "true",
".write": "auth.uid != null"
}
}
In my opinion the problem was this:
I was accessing Firebase using this method, it is not the basic method of reading firebase service data
let baseUrl = Constants.baseUrl + ".JSON"
// where baseUrl is just http path to my database and if you add ".JSON" to it - you
// open the contents of the database in Json
guard let url = URL(string: baseUrl ) else { return }
URLSession.shared.dataTask(with: url) {data, response, error in
Then I used the given Data to parse through the model:
let jsonString = String(data: data, encoding: .utf8),
let downloadedDataAsModel = Mapper<CatalogueParts>().mapArray(JSONString: jsonString)
The problem is that using this method, regardless of the rules - you get the data in any case, while the ObjectMapper processes and creates an instance, but it is incomplete, and no error occurs here
let downloadedDataAsModel = Mapper<CatalogueParts>().mapArray (JSONString: jsonString)
Therefore, this method does not allow rule settings other than "read : true"
I tried using this method and everything really worked - depending on the authorized status, access was allowed or restricted. Now it remains to decide how to use this method to get data in the form of Json for further processing =)
func fetchData(){
let ref = Constants.ref
ref.child("CatalogueParts").getData { error, snapshot in
guard error == nil else {
print(error!.localizedDescription)
return;
}
let someValue = snapshot.value
print(someValue)
}
}
could you give me a hand here? I would like to know how to call a swift function once another one is over, I think it is calling both functions at the same time. This is my viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
downloadJsonEscuelas(id_responsable: "5")
downloadJsonAnuncios(id_escuela:("\arrayEscuelas[0].id_escuela!)"))
}
The first function "downloadJsonEscuelas" fills the array "arrayEscuelas" with data, and the second function receives the data as a parameter:
downloadJsonEscuelas:
func downloadJsonEscuelas(id_responsable: String) {
guard let downloadURL = URL(string: Constantes.URLbase+"json/getescuelas.php?id="+id_responsable) else { return }
URLSession.shared.dataTask(with: downloadURL) { data, urlResponse,
error in
guard let data = data, error == nil, urlResponse != nil else {
print("Something went wrong")
return
}
print("Escuelas downloaded")
do
{
let downloadedEscuelas = try JSONDecoder().decode([Escuelas].self, from: data)
self.arrayEscuelas = downloadedEscuelas
DispatchQueue.main.async {
print("id escuela actual:"+self.idEscuelaActual!+":)")
self.escuelaTextBox.text = self.arrayEscuelas[0].escuela!
self.idEscuelaActual = "\(self.arrayEscuelas[0].id_escuela!)" as String
}
} catch let jsonErr {
print("Error: ", jsonErr)
}
}.resume()
}
downloadJsonAnuncios:
func downloadJsonAnuncios(id_escuela: String) {
guard let downloadURL = URL(string: Constantes.URLbase+"json/getanuncios.php?id="+id_escuela) else { return }
URLSession.shared.dataTask(with: downloadURL) { data, urlResponse,
error in
guard let data = data, error == nil, urlResponse != nil else {
print("Something went wrong")
return
}
print("downloaded")
do
{
let downloadedAnuncios = try JSONDecoder().decode([Anuncios].self, from: data)
self.arrayAnuncios = downloadedAnuncios
print(self.arrayAnuncios[0].titulo!)
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let jsonErr {
print("Error: ", jsonErr)
}
}.resume()
}
I think it calls both functions at the same time, so it's not filling the array in the first function. When I pass the parameter as a plain number everything goes well, something like:
downloadJsonAnuncios(id_escuela: "2")
I hope you could help me here, thank you so much.
Insert second call ( this downloadJsonAnuncios(id_escuela:("\arrayEscuelas[0].id_escuela!)")) ) inside the first call ( downloadJsonEscuelas(id_responsable: "5")) 's completion
func downloadJsonEscuelas(id_responsable: String) {
guard let downloadURL = URL(string: Constantes.URLbase+"json/getescuelas.php?id="+id_responsable) else { return }
URLSession.shared.dataTask(with: downloadURL) { data, urlResponse,
error in
guard let data = data, error == nil, urlResponse != nil else {
print("Something went wrong")
return
}
print("Escuelas downloaded")
do
{
let downloadedEscuelas = try JSONDecoder().decode([Escuelas].self, from: data)
self.arrayEscuelas = downloadedEscuelas
DispatchQueue.main.async {
print("id escuela actual:"+self.idEscuelaActual!+":)")
self.escuelaTextBox.text = self.arrayEscuelas[0].escuela!
self.idEscuelaActual = "\(self.arrayEscuelas[0].id_escuela!)" as String
}
// here
downloadJsonAnuncios(id_escuela:("\arrayEscuelas[0].id_escuela!)"))
} catch let jsonErr {
print("Error: ", jsonErr)
}
}.resume()
}
Why not call the second one in the closure of the first one
do {
let downloadedEscuelas = try JSONDecoder().decode([Escuelas].self, from: data)
self.arrayEscuelas = downloadedEscuelas
DispatchQueue.main.async {
print("id escuela actual:"+self.idEscuelaActual!+":)")
self.escuelaTextBox.text = self.arrayEscuelas[0].escuela!
self.idEscuelaActual = "\(self.arrayEscuelas[0].id_escuela!)" as String
}
downloadJsonAnuncios(id_escuela:("\arrayEscuelas[0].id_escuela!)"))
} catch let jsonErr {
print("Error: ", jsonErr)
}
}.resume()
By default the code in viewDidLoad runs synchronously on the UIThread.
Your API call is running on a separate thread and is allowing your other method to be executed before it's completed.
You should probably be using a completion handler in your method so your second method only gets called after your api call completes.
See this answer for implementing CompletionHandler
I have a lambdaInvoker which seems to hang when I am connected to a network but the connection is not active. For example when you connect to a network but have not authenticated yet. So the wifi connection is on and connected but you have not validated yet.
When the wifi/cellular is off the function immediately returns the error you would expect.
In this case my function seems to run continuously with no callback.
Is there anyway to manually set a timeout for the invoker?
let lambdaInvoker = AWSLambdaInvoker.default()
lambdaInvoker.invokeFunction(lambdaFunctionString, jsonObject: jsonObject).continueWith(block: {(task:AWSTask<AnyObject>) -> Any? in
if let error = task.error as NSError? {
print(task.error!.localizedDescription)
print(task.error!)
DispatchQueue.main.async(execute: {
if (error.domain == AWSLambdaInvokerErrorDomain) && (AWSLambdaInvokerErrorType.functionError == AWSLambdaInvokerErrorType(rawValue: error.code)) {
print("Function error: \(String(describing: error.userInfo[AWSLambdaInvokerFunctionErrorKey]))")
completion(nil)
} else {
print("Error: \(error)")
completion(nil)
}
})
return nil
}
// Handle response in task.result
DispatchQueue.main.async(execute: {
if let jsonArray = task.result as? NSArray {
completion(jsonArray)
} else {
completion(nil)
}
})
return nil
})
Trying to setup a unit test which performs a network request and attempts to serialize the response. I'm currently getting the error: Ambiguous reference to member 'jsonObject(with:options:)'. Confused as to why this is happening as the unit test should know what JSONSerialization. is?
func testAccessKeys() {
let expected = expectation(description: "Run the Access request")
sut.request(.Access, data: nil) { finished, response in
if response != nil && finished == true {
guard let json = try? JSONSerialization.jsonObject(with: response!, options: .mutableContainers) as! [String:Any] else { return XCTFail("Access request was not a dictionary")}
XCTAssertNotNil(json?["id"])
expected.fulfill()
} else {
XCTFail("Access response was nil")
}
}
waitForExpectations(timeout: 3) { error in
if let error = error {
XCTFail("Access request failure: \(error)")
}
}
}
Make sure that response is of type Data or InputStream.
These are the only types that are accepted by this function as you can see in the documentation
I have a below function in my network class which do the Alamofire Request process. I'm calling this function in my model class and when Alamofire finishes the request it calls delegate function which notify model class. My problem is that I'm calling this Alamofire function multiple times and both should finish before notifying model class. Now I handle it with very dummy way. As I searched, DispatchGroup can be used but I can't figure it out, how to implement it. Thank you.
Model Class
#objc func refresh_fiks(){
let network = Network()
network.delegate = self
self.teams = [[]]
network.getRequest(req: 1)
network.getRequest(req: 2)
}
Request Function:
func response(){
print(response_json.count)
if(response_json.count == path){
self.delegate?.sendJson(response_json)
}
}
func getRequest(req: Int) {
path = req
let rot = Router(method: .get, path: req, parameters: nil)
Alamofire.request(rot)
.response { response in
print(response.request?.url! as Any)
// check for errors
guard response.error == nil else {
// got an error in getting the data, need to handle it
print(response.error!)
let errorJson: JSON = [ "Error" : "Can't get the data!"]
self.response_json.append(errorJson)
self.response()
return
}
// make sure we got some JSON since that's what we expect
guard (response.data?.base64EncodedString()) != nil else {
print("Error: \(String(describing: response.error))")
let errorJson: JSON = [ "Error" : "Can't get the data!"]
self.response_json.append(errorJson)
self.response()
return
}
guard response.response?.statusCode == 200 else{
let errorJson: JSON = [ "Error" : "Can't get the data!"]
self.response_json.append(errorJson)
self.response()
return
}
let json = JSON(data: response.data!)
// get and print the title
if json != nil{
self.response_json.append(json)
self.response()
} else {
let errorJson: JSON = [ "Error" : "Can't get the data!"]
self.response_json.append(errorJson)
self.response()
return
}
}
}
I've made few changes in your code:
Network class:
func response(array: [JSON]){
print(array.count)
if(array.count == path){
self.delegate?.sendJson(array)
}
}
func getMultipleRequests(_ requests: [Int]) {
DispatchQueue.global(qos: .background).async {
let group = DispatchGroup()
var array: [JSON] = []
for request in requests {
group.enter()
self.getRequest(req: request, completion: { (json) in
array.append(json)
group.leave()
})
}
group.wait()
//this line below won't be called until all entries will leave the group
self.response(array: array)
}
}
func getRequest(req: Int, completion: #escaping (_ json: JSON) -> Void) {
path = req
let rot = Router(method: .get, path: req, parameters: nil)
Alamofire.request(rot)
.response { response in
print(response.request?.url! as Any)
// check for errors
guard response.error == nil else {
// got an error in getting the data, need to handle it
print(response.error!)
let errorJson: JSON = [ "Error" : "Can't get the data!"]
completion(errorJson)
return
}
// make sure we got some JSON since that's what we expect
guard (response.data?.base64EncodedString()) != nil else {
print("Error: \(String(describing: response.error))")
let errorJson: JSON = [ "Error" : "Can't get the data!"]
completion(errorJson)
return
}
guard response.response?.statusCode == 200 else{
let errorJson: JSON = [ "Error" : "Can't get the data!"]
completion(errorJson)
return
}
let json = JSON(data: response.data!)
// get and print the title
if json != nil{
completion(json)
} else {
let errorJson: JSON = [ "Error" : "Can't get the data!"]
completion(errorJson)
return
}
}
}
So the getRequest function now have a completion block that will return a json result off each request and the function getMultipleRequests that will receive a bunch of requests from anyone
This how you can use it
Your class, that calls refresh_fiks:
#objc func refresh_fiks(){
let network = Network()
network.delegate = self
self.teams = [[]]
network.getMultipleRequests([1,2])
}
Also, instead of using group.wait() you might need to use group.notify, it's better to notify that all entries leaved the group in specified queue, like the main:
group.notify(queue: DispatchQueue.main, execute: {
print("All Done")
self.response(array: array)
})
What to read about the DispatchGroups:
RayWenderlich
ALL ABOUT SWIFT