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
相关产品推荐
相关产品推荐

