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

Swift API调用单元测试编写求助:QuotesModel测试方案咨询

为Swift API调用编写单元测试的方案

你的QuotesModel直接依赖URLSession.shared,这会导致单元测试依赖真实网络,无法精准控制测试场景。我们需要先做依赖注入,把网络请求逻辑抽象出来,再编写模拟实现用于测试。

步骤1:抽象网络请求协议

定义一个协议封装网络数据请求行为,让QuotesModel依赖这个抽象协议而非具体的URLSession:

protocol NetworkServiceProtocol {
    func data(from url: URL) async throws -> (Data, URLResponse)
}

// 让URLSession遵守协议,用于生产环境
extension URLSession: NetworkServiceProtocol {}

步骤2:修改QuotesModel,注入依赖

更新QuotesModel,将硬编码的URLSession.shared替换为可注入的网络服务实例,同时把请求URL也改成可配置项(方便测试无效URL场景):

class QuotesModel: ObservableObject {
    
    @Published var quotes = [Quote]()
    private let networkService: NetworkServiceProtocol
    private let quoteURL: URL

    // 初始化时注入依赖,默认使用生产环境配置
    init(networkService: NetworkServiceProtocol = URLSession.shared,
         quoteURL: URL = URL(string: "https://breakingbadapi.com/api/quotes")!) {
        self.networkService = networkService
        self.quoteURL = quoteURL
    }

    @MainActor  
    func fetchData() async {
        guard quoteURL.scheme != nil else {
            print("Invalid URL")
            return
        }
        do {
            let (data, _) = try await networkService.data(from: quoteURL)
            quotes = try JSONDecoder().decode([Quote].self, from: data)
        } catch {
            print(error)
        }
    }
}

步骤3:编写模拟网络服务(Mock)

创建一个模拟网络服务,用于测试时返回预设的假数据或抛出指定错误:

class MockNetworkService: NetworkServiceProtocol {
    // 可配置的返回结果,测试时按需设置
    var mockResult: Result<(Data, URLResponse), Error>?

    func data(from url: URL) async throws -> (Data, URLResponse) {
        guard let result = mockResult else {
            throw NSError(domain: "MockError", code: -1, userInfo: [NSLocalizedDescriptionKey: "未设置模拟结果"])
        }
        return try result.get()
    }
}

步骤4:编写单元测试用例

使用XCTest编写测试,覆盖成功解析、网络失败、无效URL三种核心场景:

import XCTest
import Combine
@testable import YourAppName // 替换成你的App模块名

final class QuotesModelTests: XCTestCase {
    private var model: QuotesModel!
    private var mockNetworkService: MockNetworkService!
    private var cancellables = Set<AnyCancellable>()

    // 测试前初始化对象
    override func setUp() {
        super.setUp()
        mockNetworkService = MockNetworkService()
        model = QuotesModel(networkService: mockNetworkService)
    }

    // 测试后清理资源
    override func tearDown() {
        model = nil
        mockNetworkService = nil
        cancellables.removeAll()
        super.tearDown()
    }

    // 测试场景1:成功获取并解析数据
    func testFetchData_Success() async {
        // 1. 准备测试用的假数据
        let testQuotes = [Quote(quote: "测试台词", author: "测试作者", id: 1)]
        let jsonData = try! JSONEncoder().encode(testQuotes)
        let mockResponse = HTTPURLResponse(url: model.quoteURL, statusCode: 200, httpVersion: nil, headerFields: nil)!
        
        // 2. 设置Mock返回成功结果
        mockNetworkService.mockResult = .success((jsonData, mockResponse))

        // 3. 调用目标方法
        await model.fetchData()

        // 4. 验证结果是否符合预期
        XCTAssertEqual(model.quotes.count, 1)
        XCTAssertEqual(model.quotes.first?.quote, "测试台词")
    }

    // 测试场景2:网络请求失败
    func testFetchData_NetworkFailure() async {
        // 1. 设置Mock返回错误
        let testError = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "网络请求失败"])
        mockNetworkService.mockResult = .failure(testError)

        // 2. 调用目标方法
        await model.fetchData()

        // 3. 验证数据未被修改
        XCTAssertTrue(model.quotes.isEmpty)
    }

    // 测试场景3:无效URL
    func testFetchData_InvalidURL() async {
        // 1. 传入无效URL初始化Model
        let invalidURL = URL(string: "invalid-url")!
        model = QuotesModel(networkService: mockNetworkService, quoteURL: invalidURL)
        
        // 2. 调用目标方法
        await model.fetchData()
        
        // 3. 验证数据未被修改
        XCTAssertTrue(model.quotes.isEmpty)
    }
}

关键注意点

  • 依赖注入:让Model依赖抽象协议而非具体实现,是单元测试的核心,这样才能完全控制测试环境,避免依赖真实网络。
  • Mock的灵活性:通过配置Mock的返回结果,可以覆盖各种边界场景,比如错误的JSON格式、非200状态码等,只需要调整mockResult的设置即可。
  • @MainActor处理:因为fetchData标记了@MainActor,测试时直接用await调用即可,XCTest会自动处理主线程相关逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 09:42:05