英文:
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 the test folder. I am expecting to pass, but it's showing that it found the record 0 instead of returning the 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 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 should return 40 records, but it returns 0.
    }
}
Here is the link for JSON:
https://fruityvice.com/api/fruit/all
英文:
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 ..
答案1
得分: 0
`testTotalCountWithSuccess`测试的是异步代码。当你在第23行调用`fetchFruit`时,会发生异步操作。即使你使用了MockService,完成处理程序将在*稍后的某个时间点*被调用。这个时间点在你在第24和25行检查`fruits`计数之后。所以当你执行XCTAssert时,你没有值可以进行比较。
在这里需要做的是使用[XCTestExpectation][1],如下所示:
```swift
let expectation = expectation(description: "Wait for async code to complete")
service.load { 
    // 在加载完成后
    expectation.fulfill()
}
wait(for: [expectation], timeout: 2.0)
在你的情况下,你需要更新viewModel的fetchFruit函数,使其具有一个作为参数的完成闭包,你可以在测试中传递。类似于:
func fetchFruit(completion: ((Bool) -> ())? = nil) {
    //...(函数体不变)
}
viewModel.fetchFruit { result in ... }
<details>
<summary>英文:</summary>
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][1] 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 ... }
   
  [1]: https://developer.apple.com/documentation/xctest/xctestexpectation
</details>
				通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。



评论