Search code examples
swiftunit-testingswiftuimvvm

Swift UI Application falling on Unit test cases


I create the app into Swift UI . I wrote the Unit testing case . I have local json into test folder . I am expecting to pass but it showing it found the record 0 instead of returning total record . Here is the error message ..

testTotalCountWithSuccess(): XCTAssertEqual failed: ("0") is not equal to ("40") - Total record is matched Here is code for Content view ..

import SwiftUI

struct ContentView: View {

    @EnvironmentObject private var viewModel: FruitListViewModel
    @State private var filteredFruits: [Fruits] = []
    @State var searchText = ""

    var body: some View {
        NavigationView {
            List {
                ForEach(fruits) { fruit in
                    NavigationLink(destination: FruitDetailsView(fruit: fruit)) {
                        RowView(name: fruit.name, genus: fruit.genus, family: fruit.family)
                    }
                }
            }
            .searchable(text: $searchText)
            .onChange(of: searchText, perform: performSearch)
            .task {
                viewModel.fetchFruit()
            }
            .navigationTitle("Fruits List")
        }

        .onAppear {

            viewModel.fetchFruit()
        }
    }
    private func performSearch(keyword: String) {
        filteredFruits = viewModel.fruits.filter { fruit in
        fruit.name.contains(keyword)
        }
    }
    private var fruits: [Fruits] {
        filteredFruits.isEmpty ? viewModel.fruits: filteredFruits
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Here is the view model ..

protocol FruitListViewModelType {
    func fetchFruit()
}

class FruitListViewModel: FruitListViewModelType, ObservableObject {

    private let service:Service!
    @Published private(set) var fruits = [Fruits]()

    init(service:Service = ServiceImpl()) {
        self.service = service
    }

    func fetchFruit() {
        let client = ServiceClient(baseUrl:EndPoints.baseUrl.rawValue, path:Path.cakeList.rawValue, params:"", method:"get")

        service.fetchData(client:client, type:[Fruits].self) { [weak self] (result)  in
            switch result {
            case .success(let result):
                DispatchQueue.main.async {
                    self?.fruits = result
                }

            case .failure(let error):
                DispatchQueue.main.async {
                    print(error.localizedDescription)
                    self?.fruits = []
                }
            }
        }
    }
}

Here is the my Mock api class .

import Foundation
@testable import FruitsDemoSwiftUI

class MockService: Service, JsonDecodable {
    
    var responseFileName = ""

    func fetchData<T>(client: ServiceClient, type: T.Type, completionHandler: @escaping Completion<T>) where T : Decodable, T : Encodable {
        // Obtain Reference to Bundle
        let bundle = Bundle(for:MockService.self)

        guard let url = bundle.url(forResource:responseFileName, withExtension:"json"),
              let data = try? Data(contentsOf: url),
              let output = decode(input:data, type:T.self)
        else {
            completionHandler(.failure(ServiceError.parsinFailed(message:"Failed to get response")))
            return
        }
        completionHandler(.success(output))
    }
}

Here is the unit test code ..

import XCTest
@testable import FruitsDemoSwiftUI

final class FruitsDemoSwiftUITests: XCTestCase {

    var mockService: MockService!
    var viewModel: FruitListViewModel!

    override func setUp() {
        mockService = MockService()
        viewModel = FruitListViewModel(service: mockService)
    }

    func testTotalCountWithSuccess() {
        mockService.responseFileName = "FruitSuccessResponse"
        viewModel.fetchFruit()
        let resultCount = viewModel.fruits.count
        XCTAssertEqual(resultCount ,40, "Total record is  matched")// it shroud return 40 record but it return 0.
    }
}

Here is the link for Json ..

https://fruityvice.com/api/fruit/all

Here is the screenshot .. enter image description here


Solution

  • The testTotalCountWithSuccess test is testing asynchronous code. When you call fetchFruit on line 23, an async operation takes place. Even though you are using your MockService, the completion handler will be called at some later point in time. This point in time is after you are checking against your fruits count on line 24 and 25. So by the time you are executing XCTAssert you have no value to compare against.

    What you need to do here is to use XCTestExpectation as follows:

      let expectation = expectation(description: "Wait for async code to complete")
      
      service.load { 
          // after load completes
          expectation.fulfill()
      }
    
      wait(for: [expectation], timeout: 2.0)
    

    In your case you'll need to update your viewModel's fetchFruit function to have a completion closure as a parameter that you can pass from within your test. Something like:

    func fetchFruit(completion: ((Bool) -> ())? = nil) {
        let client = ServiceClient(baseUrl:EndPoints.baseUrl.rawValue, path:Path.cakeList.rawValue, params:"", method:"get")
    
        service.fetchData(client:client, type:[Fruits].self) { [weak self] (result)  in
            switch result {
            case .success(let result):
                DispatchQueue.main.async {
                    self?.fruits = result
                    completion?(true)
                }
    
            case .failure(let error):
                DispatchQueue.main.async {
                    print(error.localizedDescription)
                    self?.fruits = []
                    completion?(false)
                }
            }
        }
    }
    

    And then calling it like:

    viewModel.fetchFruit { result in ... }