Swift Test case Falling with Expectation - swift

I am trying to run test case for Failure response . I have an empty json file into project and named it FailureResponse . This file is empty . I trying to count the number of array is empty for example ..
XCTAssertTrue(schools.count==0)
It should pass the test because the json file is empty .
same result fields like school name and School location etc but the problem is it showing error ..
testFailure(): Asynchronous wait failed: Exceeded timeout of 6 seconds, with unfulfilled expectations: "waiting for response".
View Model code...
import Foundation
import Combine
class ViewModel {
private let networkManager = NetworkManager()
#Published private(set) var school = [School]()
func getSchools() {
loadMoreSchools()
}
func loadMoreSchools() {
let newURL = NetworkURLs.baseURL
networkManager
.getModel([School].self, from: newURL) { [weak self] result in
switch result {
case .success(let schoolResponse):
self?.school = schoolResponse
print(schoolResponse)
case .failure(let error):
print(error)
}
}
}
func getSchoolName(by row: Int) -> String {
let schoolName = school[row]
return schoolName.schoolName.uppercased()
}
func getSchoolLocation(by row: Int) -> String {
return "\(school[row].location)"
}
}
Here is my Mock service call ..
class MockService: NetworkManagerProtocol {
var data: Data?
func getModel<Model>(_ type: Model.Type, from url: String, completion: #escaping (Result<Model, Alomafire_Project.NetworkError>) -> ()) where Model : Decodable, Model : Encodable {
if let data = data {
do {
let result = try JSONDecoder().decode(type, from: data)
completion(.success(result))
} catch (let error){
print(error)
}
}
}
}
Here is code for call the local Jason ..
func getData(json: String) throws -> Data {
guard let url = Bundle(for: Alomafire_ProjectTests.self).url(forResource: json, withExtension: "json")
else { return Data() }
return try Data(contentsOf: url)
}
Here is the test case ....
func testFailure() throws {
// Given
mockService.data = try getData(json: "FailureResponse")
var schools: [School] = []
let expectation = expectation(description: "waiting for response")
// When
viewModel?
.$school
.dropFirst()
.sink(receiveValue: { result in
schools = result
expectation.fulfill()
})
.store(in: &subscribers)
// viewModel?.getSchools()
// Then
waitForExpectations(timeout: 10.0)
XCTAssertTrue(schools.count==0)
}
Here is the debug result . it return 0 ..
Here is the screenshot of the result ..

You mention in the question that "the json file is empty." If that is the case, then this test will fail. The MockService assumes that the Data pulled from the json file will be decodable to the type requested. If it isn't the getModel(_:from:completion:) will never call the completion and the test will not complete in the specified time limit. Solve this by calling the completion closure even when the JSONDecoder response with an error.
Also, even if that mock emits the error properly, your ViewModel doesn't do anything with it that would cause the schools type to update.

Related

Swift CoreData - objects not being inserted in to managed object context

I've reworked this question after further research and in response to comments that it was too long.
I am downloading and decoding data, in CSV format using CodableCSV, from three URLs and I've been able to confirm that I am receiving all the data I expect (as of today, 35027 lines). As the data is decoded, I am injecting a NSManagedObjectContext in to the decoded object. Here is my managed object class:
import Foundation
import CoreData
#objc(MacListEntry)
class MacListEntry: NSManagedObject, Decodable {
//var id = UUID()
#NSManaged var registry: String?
#NSManaged var assignment: String?
#NSManaged var org_name: String?
#NSManaged var org_addr: String?
required convenience init(from decoder: Decoder) throws {
guard let keyManObjContext = CodingUserInfoKey.managedObjectContext,
let context = decoder.userInfo[keyManObjContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "MacListEntry", in: context) else {
fatalError("Failed to receive managed object context")
}
self.init(entity: entity, insertInto: context)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.registry = try container.decode(String.self, forKey: .registry)
self.assignment = try container.decode(String.self, forKey: .assignment)
self.org_name = try container.decode(String.self, forKey: .org_name)
self.org_addr = try container.decode(String.self, forKey: .org_addr)
}
private enum CodingKeys: Int, CodingKey {
case registry = 0
case assignment = 1
case org_name = 2
case org_addr = 3
}
}
public extension CodingUserInfoKey {
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")
}
I then attempt to save the context using try context.save() but before doing so, examine the numbers of records I am trying to insert using:
print("Deleted objects: (self.persistentContainer.viewContext.deletedObjects.count)")
print("Inserted objects: (self.persistentContainer.viewContext.insertedObjects.count)")
print("Has changes: \(self.persistentContainer.viewContext.hasChanges)")
and get a different number of inserted records every time the code runs - always short, by around 0.5%. I am struggling to understand under what circumstances objects added to a managed object context in this way simply don't appear in the list of inserted objects and don't make it in to the saved database. Is there a practical limit on the number of records inserted in one go?
Can anyone suggest where else I should be looking - the error is tiny enough that it looks like the program is running fine, but it isn't.
Many thanks.
I think I've found the problem and if I'm correct, posting my solution here may help others. I'm downloading the three CSV files using Combine and dataTaskPublisher - creating three separate publishers and then merging them in to a single stream. While I was aware that I couldn't update the UI on a background thread so had added the .receive(on: DispatchQueue.main) in the chain (indicated by (1) in the code below), I put it AFTER the .tryMap { } where the decoding was happening. Because it was the process of decoding that inserted the object in to the managed object context, this must have been happening asynchronously and hence causing problems. By moving the .receive(on: ...) line ABOVE the .tryMap (see (2) below), this appears to have resolved the problem - or at least made the fetch, decode and insert return the correct number of inserted records consistently.
enum RequestError: Error {
case sessionError(error: HTTPURLResponse)
}
struct Agent {
let decoder: CSVDecoder
struct Response<T> {
let value: T
let response: URLResponse
}
func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<Response<T>, Error> {
print("In run()")
return URLSession.shared
.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main) // -- (1)
.tryMap { result -> Response<T> in
print(result)
guard let httpResponse = result.response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw RequestError.sessionError(error: result.response as! HTTPURLResponse)
}
let value = try self.decoder.decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
//.receive(on: DispatchQueue.main). // -- (2)
.eraseToAnyPublisher()
}
}
struct Endpoint {
var agent: Agent
let base: String
}
extension Endpoint {
func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
return agent.run(request)
.map(\.value)
.eraseToAnyPublisher()
}
func fetch(thing: String) -> AnyPublisher<[MacListEntry], Error> {
return run(URLRequest(url: URL(string: base+thing)!))
}
}
struct Response: Codable {
let name: String
}
--- snip ---
var requests:[AnyPublisher<[MacListEntry],Error>] = []
requests.append(endpoint.fetch(thing: "file1.csv"))
requests.append(endpoint.fetch(thing: "file2.csv"))
requests.append(endpoint.fetch(thing: "file3.csv"))
let _ = Publishers.MergeMany(requests)
.sink(receiveCompletion: { completion in
...
)
.store(in: &bag)

Having trouble loading custom data from firebase using swift

I followed the directions on the firebase website and came up with the code below labeled NEW CODE. The error I am getting is:
Cannot invoke initializer for type 'Result' with an argument list of type '(#escaping () throws -> CombinedModel?)'
I have tried researching about the Result object and this error but have not found anything that would help me directly. I was able to read the data using the old way (which I will post below), but am trying to follow Googles documentation
(https://firebase.google.com/docs/firestore/query-data/get-data)
Any help would be appreciated, thank you!
OLD CODE
func readData(word: String) -> CombinedModel? {
print("reading data")
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
var wordResults: CombinedModel? = nil
docRef.getDocument { (document, error) in
if let e = error {
print("Error loading data: \(e)")
return
} else {
do {
if let resultData = try document?.data(as: CombinedModel.self){
print("Definitions: \(resultData.definitionsArray)")
print("\n Synonyms: \(resultData.synonyms)")
wordResults = resultData
}
} catch {
print("Error decoding: \(error)")
}
}
}
return wordResults
}
NEW CODE
func newReadData(word: String) -> CombinedModel? {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
switch result {
case .success(let combinedModel):
if let combinedModel = combinedModel {
print("CombinedModel: \(combinedModel)")
} else {
print("Document does not exist")
}
case .failure(let error):
print("Error decoding city: \(error)")
}
}
}
Here is the codable custom class I created for reading the data.
struct CombinedModel: Codable {
var definitionsArray: [WordModel]
var synonyms: [String]
private enum CodingKeys: String, CodingKey {
case definitionsArray
case synonyms
}
}
struct WordModel: Codable {
let id: String
let partOfSpeech: String
let definitions: [String]
let example: [String]
let ipa: String
let audio: String
private enum CodingKeys: String, CodingKey {
case id
case partOfSpeech
case definitions
case example
case ipa
case audio
}
}
struct ThesaurusModel: Codable {
var synonyms: [String]
private enum CodingKeys: String, CodingKey {
case synonyms
}
}
I realized the issue with the Result object was that Xcode was reading it as a structure that I created in a different file, but once I changed the name of that structure(it was named Result previously), it recognized the Result object as the correct object which is of type: enum Result where Failure : Error
Looks like you're getting close - minus the type error. From what I can tell, your error:
Cannot invoke initializer for type 'Result' with an argument list of type '(#escaping () throws -> CombinedModel?)'
Refers to this portion of code:
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
At least, this is the portion of code that looks off to me. Your (document, error) variable as a whole represents your result. You shouldn't need a Result middleman.
The real focus should be around determining whether or not there is an error. If there isn't you should read the document.
A working approach should look something like this:
func newReadData(word: String) -> CombinedModel? {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
guard let fbDoc = document else {
// document is nil, parse the error object and handle it.
}
if let resultData = try fbDoc?.data(as: CombinedModel.self) {
return resultData
} else {
return nil
}
}
}
Let me know how that goes for you! Best of luck.

Loop over Publisher Combine framework

I have the following function to perform an URL request:
final class ServiceManagerImpl: ServiceManager, ObservableObject {
private let session = URLSession.shared
func performRequest<T>(_ request: T) -> AnyPublisher<String?, APIError> where T : Request {
session.dataTaskPublisher(for: self.urlRequest(request))
.tryMap { data, response in
try self.validateResponse(response)
return String(data: data, encoding: .utf8)
}
.mapError { error in
return self.transformError(error)
}
.eraseToAnyPublisher()
}
}
Having these 2 following functions, I can now call the desired requests from corresponded ViewModel:
final class AuditServiceImpl: AuditService {
private let serviceManager: ServiceManager = ServiceManagerImpl()
func emptyAction() -> AnyPublisher<String?, APIError> {
let request = AuditRequest(act: "", nonce: String.randomNumberGenerator)
return serviceManager.performRequest(request)
}
func burbleAction(offset: Int) -> AnyPublisher<String?, APIError> {
let request = AuditRequest(act: "burble", nonce: String.randomNumberGenerator, offset: offset)
return serviceManager.performRequest(request)
}
}
final class AuditViewModel: ObservableObject {
#Published var auditLog: String = ""
private let auditService: AuditService = AuditServiceImpl()
init() {
let timer = Timer(timeInterval: 5, repeats: true) { _ in
self.getBurbles()
}
RunLoop.main.add(timer, forMode: .common)
}
func getBurbles() {
auditService.emptyAction()
.flatMap { [unowned self] offset -> AnyPublisher<String?, APIError> in
let currentOffset = Int(offset?.unwrapped ?? "") ?? 0
return self.auditService.burbleAction(offset: currentOffset)
}
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [unowned self] completion in
print(completion)
}, receiveValue: { [weak self] burbles in
self?.auditLog = burbles!
})
.store(in: &cancellableSet)
}
}
Everything is fine when I use self.getBurbles() for the first time. However, for the next calls, print(completion) shows finished, and the code doesn't perform self?.auditLog = burbles!
I don't know how can I loop over the getBurbles() function and get the response at different intervals.
Edit
The whole process in a nutshell:
I call getBurbles() from class initializer
getBurbles() calls 2 nested functions: emptyAction() and burbleAction(offset: Int)
Those 2 functions generate different requests and call performRequest<T>(_ request: T)
Finally, I set the response into auditLog variable and show it on the SwiftUI layer
There are at least 2 issues here.
First when a Publisher errors it will never produce elements again. That's a problem here because you want to recycle the Publisher here and call it many times, even if the inner Publisher fails. You need to handle the error inside the flatMap and make sure it doesn't propagate to the enclosing Publisher. (ie you can return a Result or some other enum or tuple that indicates you should display an error state).
Second, flatMap is almost certainly not what you want here since it will merge all of the api calls and return them in arbitrary order. If you want to cancel any existing requests and only show the latest results then you should use .map followed by switchToLatest.

Cannot convert value of type 'MyEnum<T.Type>' to expected argument type 'MyEnum<_>'

I have a network layer working with generics and I'm using protocols so I can test it later. I have followed this tutorial https://medium.com/thecocoapps/network-layer-in-swift-4-0-972bf2ea5033
This is my Mock for testing:
import Foundation
#testable import TraktTest
class MockUrlSessionProvider: ProviderProtocol {
enum Mode {
case success
case empty
case fail
}
private var mode: Mode
init(mode: Mode) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
switch mode {
case .success: completion(NetworkResponse.success(T))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}
I'm getting the error: Cannot convert value of type 'NetworkResponse<T.Type>' to expected argument type 'NetworkResponse<_>' in this line: completion(NetworkResponse.success(T))
If I send this to my completion success it compile:
try? JSONDecoder().decode(T.self, from: data!)
(dummy data that I created using encode and my model), but crash when get to my model because is nil despite I had encoded using JSONEncoder() with a correct model.
I think it works, because is the same logic that I use in my class that implements ProviderProtocol in my app:
final class URLSessionProvider: ProviderProtocol {
private var session: URLSessionProtocol
init(session: URLSessionProtocol = URLSession.shared) {
self.session = session
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
let request = URLRequest(service: service)
session.dataTask(request: request) { [weak self] data, response, error in
let httpResponse = response as? HTTPURLResponse
self?.handleDataResponse(data: data, response: httpResponse, error: error, completion: completion)
}.resume()
}
private func handleDataResponse<T: Decodable>(data: Data?, response: HTTPURLResponse?, error: Error?, completion: (NetworkResponse<T>) -> Void) {
guard error == nil else { return completion(.failure(.unknown(error?.localizedDescription ?? "Error"))) }
guard let response = response else { return completion(.failure(.unknown("no_response".localized()))) }
switch response.statusCode {
case 200...299:
guard let data = data, let model = try? JSONDecoder().decode(T.self, from: data) else { return completion(.failure(.noData)) }
completion(.success(model))
default: completion(.failure(.unknown("no_response".localized())))
}
}
}
URLSessionProtocol is just a protocol which has a method dataTask same as the one in URLSession.shared (receive a URLRequest and returns Data, Response and Error in a completion).
My Network responses are a couple of enums:
enum NetworkResponse<T> {
case success(T)
case failure(NetworkError)
}
enum NetworkError {
case unknown(String)
case noData
}
My provider protocol just have a function to make the request using generics:
protocol ProviderProtocol {
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping(NetworkResponse<T>) -> Void) where T: Decodable
}
I don't think I need to use ServiceProtocol in my test because is to setup the request with endpoint, headers, body, id, etc. But this is the protocol I created:
typealias Headers = [String: String]
typealias Parameters = [String: Any]
protocol ServiceProtocol {
func baseURL() -> URL
var path: String? { get }
var id: String? { get }
var method: HTTPMethod { get }
var task: Task { get }
var headers: Headers? { get }
var parametersEncoding: ParametersEncoding { get }
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
}
enum Task {
case requestPlain
case requestParameters(Parameters)
}
enum ParametersEncoding {
case url
case json
}
In my app, I have a class that implement ProviderProtocol and use a URLSession.shared to make the dataTask when some viewModel call the request with the appropiate model.
I'm use to make test with protocols and a specific model, but with generics is showing me that error. How can I achieve to have a mock provider using generics so I can test any viewModel who make a call to network using different kinds of models (stubs).
The error occurs because NetworkResponse expects an instance of T, while the mock tries to provide the actual T.
So, you need to somehow provide an instance, however this cannot be generated by the mock as it doesn't have enough information about how to construct an instance.
I recommend injecting the success value from the outside, when creating the mock. You can do this either by making the mock class generic, or by making the Mode enum generic. Below is a sample implementation for the latter:
class MockUrlSessionProvider: ProviderProtocol {
// making the enum generic, to support injecting the success value
enum Mode<T> {
case success(T)
case empty
case fail
}
// need to have this as `Any` to cover all possible T generic arguments
private var mode: Any
// however the initializer can be very specific
init<T>(mode: Mode<T>) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
// if the mock was not properly configured, do nothing
guard let mode = mode as? Mode<T> else { return }
// alternatively you force cast and have the unit test crash, this should help catching early configuration issues
// let mode = mode as! Mode<T>
switch mode {
case let .success(value): completion(NetworkResponse.success(value))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}

When updating UI with json response, "Thread 1: Fatal error: Index out of range." is received - Swift

Attempting to update a menu item to return all fixtures from api.
I've got a list of fixtures being returned.
How do I go about updating the fixtureMenuItem in the MenuController with all fixtures returned from the JSON? I thought I might be able to do something along the lines of fixtureMenuItem.title = fixtures.description
, but I'm getting "Thread 1: Fatal error: Index out of range."
Model
struct LiveScores: Codable {
let success: Bool
let fixturesData: FixturesData?
enum CodingKeys: String, CodingKey {
case fixturesData = "data"
case success
}
}
struct FixturesData: Codable {
let fixtures: [Fixture]
let nextPage, prevPage: Bool
enum CodingKeys: String, CodingKey {
case fixtures
case nextPage = "next_page"
case prevPage = "prev_page"
}
}
struct Fixture: Codable, CustomStringConvertible {
let id, date, time, round: String
let homeName, awayName, location, leagueID: String
let homeID, awayID: Int?
enum CodingKeys: String, CodingKey {
case id, date, time, round
case homeName = "home_name"
case awayName = "away_name"
case location
case leagueID = "league_id"
case homeID = "home_id"
case awayID = "away_id"
}
var description: String {
return "\(time): \(homeName) vs. \(awayName)"
}
}
// MARK: Convenience initializers
extension LiveScores {
init(data: Data) throws {
self = try JSONDecoder().decode(LiveScores.self, from: data)
}
}
Menu Controller - this is where I want to update the fixture menu item, to include the time, home and away team names. "Here is where all the fixtures will be populated!" - this is the hardcoded text I wish to replace with the fixture data.
var fixtures = [Fixture]()
func updateScores() {
liveScoreApi.fetchFixtures()
if let fixtureMenuItem = self.Menu.item(withTitle: "Fixtures") {
fixtureMenuItem.title = "Here is where all the fixtures will be populated!"
// TODO - populate the UI with fixtures returned from JSON response
}
}
Fetch Fixtures - here's where the fixtures are retrieved.
func fetchFixtures() {
let session = URLSession.shared
let url = URL(string: "\(baseUrl)fixtures/matches.json?key=\
(apiKey)&secret=\(apiSecret)&date=2018-06-02")
let task = session.dataTask(with: url!) { data, response, err in
// check for a hard error
if let error = err {
NSLog("Live Scores Api Error: \(error)")
}
// check the response code
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200: // perfecto!
if let liveScores = try? LiveScores.init(data: data!),
let fixture = liveScores.fixturesData
{
NSLog("\(fixture)")
}
case 401: // unauthorised
NSLog("Live Score Api returned an 'unauthorised' response.")
default:
NSLog("Live Scores Api returned response: %d %#", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))
}
}
}
task.resume()
}
In this example fixture data there are 26 fixtures and I want to show all of these.
Variations of this question come up constantly on SO.
Async functions don't wait for their results to be available. You give them a callback, which is a closure (a block of code you provide) that gets executed once the operation is complete.
You should rewrite your fetchFixtures() function to take a completion handler, and then refactor your updateScores() function to pass the code that updates your menu item into the completion handler for FetchFixtures.
See my answer to the question in the thread below for a simple example of this approach:
Swift: Wait for Firebase to load before return a function
As Duncan said in his answer, the issue was that the results weren't actually available.
I've implemented a completion handler of handleCompletion: on the fetchFixtures() function, which takes a true/false value plus the fixtures data. This is then returned in each http response case as shown below:
func fetchFixtures(handleCompletion:#escaping (_ isOK:Bool,_ param:
FixturesData?)->()) {
let session = URLSession.shared
let url = URL(string: "\(baseUrl)fixtures/matches.json?key=\
(apiKey)&secret=\(apiSecret)&date=2018-06-04")
let task = session.dataTask(with: url!) { data, response, err in
// check for a hard error
if let error = err {
NSLog("Live Scores Api Error: \(error)")
}
// check the response code
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200: // perfecto!
if let liveScores = try? LiveScores.init(data: data!),
let fixture = liveScores.fixturesData
{
//NSLog("\(fixture)")
handleCompletion(true, fixture)
}
case 401: // unauthorised
NSLog("Live Score Api returned an 'unauthorised' response.")
handleCompletion(false, nil)
default:
NSLog("Live Scores Api returned response: %d %#", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))
handleCompletion(false, nil)
}
}
}
task.resume()
}
After implementing the above, I refactored the updateScores() to use this completion handler.
func updateScores() {
liveScoreApi.fetchFixtures() { (
isOK, fixture) in
if isOK == true {
if let fixtureMenuItem = self.Menu.item(withTitle: "Fixtures") {
fixtureMenuItem.title = (fixture?.fixtures.description)!
}
}
else {
NSLog("error fetching!")
}
}
}
The fixtureMenuItem now successfully displays the data if available.