You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI应用单元测试失败:预期40条记录实际返回0条

单元测试失败排查:水果列表应用返回记录数为0而非预期40条

我使用SwiftUI开发了一款水果列表应用,并编写了单元测试用例,测试文件夹中存放了本地JSON文件。预期测试能够通过,但实际运行时发现返回记录数为0,而非预期的40条。错误信息如下:
testTotalCountWithSuccess(): XCTAssertEqual failed: ("0") is not equal to ("40") - Total record is matched


ContentView代码

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()
    }
}

ViewModel代码

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 = []
                }
            }
        }
    }
}

MockService代码

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))
    }
}

单元测试代码

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 record but it return 0.
    }
}

失败原因及解决方法

1. 异步操作未等待,测试提前断言

viewModel.fetchFruit()是异步方法,调用后立刻读取viewModel.fruits.count时,MockService的回调还未执行,此时fruits仍是初始空数组。

解决方法:使用XCTestExpectation等待异步操作完成:

func testTotalCountWithSuccess() {
    let expectation = XCTestExpectation(description: "Fruits loaded successfully")
    mockService.responseFileName = "FruitSuccessResponse"
    
    // 监听fruits的Published值变化
    let cancellable = viewModel.$fruits.sink { fruits in
        if fruits.count == 40 {
            expectation.fulfill()
        }
    }
    
    viewModel.fetchFruit()
    wait(for: [expectation], timeout: 1.0)
    
    XCTAssertEqual(viewModel.fruits.count, 40, "Total record is matched")
    cancellable.cancel()
}

2. JSON文件配置问题

检查FruitSuccessResponse.json文件:

  • 确认已添加到测试Target的Bundle中(Xcode中选中文件,右侧"Target Membership"需勾选测试目标)
  • 文件名拼写完全一致(iOS Bundle区分大小写)
  • JSON格式与Fruits模型结构匹配,确保能正常解码

3. 解码逻辑异常

检查JsonDecodable协议的decode方法实现,确认它能正确将JSON数据解码为[Fruits]类型。若JSON结构与模型不匹配,会触发解码失败,导致fruits被设为空数组。

4. 主线程异步更新的延迟

即使MockService同步读取本地文件,回调中使用DispatchQueue.main.async更新fruits,主线程队列任务需等待当前测试代码执行完毕才会运行,因此测试代码会先读取到空数组。通过XCTestExpectation监听fruits的变化可解决此问题。


内容的提问来源于stack exchange,提问作者user8705895

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 08:52:51