英文:
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>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论