Swift UI 应用在单元测试用例上出现问题。

huangapple go评论82阅读模式
英文:

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

Here is the screenshot:
Swift UI 应用在单元测试用例上出现问题。

英文:

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 = &quot;&quot;

    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(&quot;Fruits List&quot;)
        }

        .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:&quot;&quot;, method:&quot;get&quot;)

        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 = &quot;&quot;

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

        guard let url = bundle.url(forResource:responseFileName, withExtension:&quot;json&quot;),
              let data = try? Data(contentsOf: url),
              let output = decode(input:data, type:T.self)
        else {
            completionHandler(.failure(ServiceError.parsinFailed(message:&quot;Failed to get response&quot;)))
            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 = &quot;FruitSuccessResponse&quot;
        viewModel.fetchFruit()
        let resultCount = viewModel.fruits.count
        XCTAssertEqual(resultCount ,40, &quot;Total record is  matched&quot;)// 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 ..
Swift UI 应用在单元测试用例上出现问题。

答案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: &quot;Wait for async code to complete&quot;)
      
      service.load { 
          // after load completes
          expectation.fulfill()
      }

      wait(for: [expectation], timeout: 2.0)


In your case you&#39;ll need to update your viewModel&#39;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) -&gt; ())? = nil) {
        let client = ServiceClient(baseUrl:EndPoints.baseUrl.rawValue, path:Path.cakeList.rawValue, params:&quot;&quot;, method:&quot;get&quot;)
    
        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>



huangapple
  • 本文由 发表于 2023年3月7日 22:32:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/75663308.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定