Swift Combine中Mock URLSession的dataTaskPublisher方法最佳方案
解决方案:Mock URLSession 测试 APIClient.dispatch 方法
核心思路
由于URLSession.DataTaskPublisher是结构体无法直接重写,我们可以通过协议抽象的方式,将URLSession的依赖抽象为协议,让APIClient依赖该协议而非具体的URLSession类,从而实现轻松Mock。
步骤1:定义协议抽象核心方法
创建一个仅包含dataTaskPublisher(for:)方法的协议,让系统URLSession自动遵守该协议,原有业务代码无需改动:
protocol URLSessionProtocol { func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher } extension URLSession: URLSessionProtocol {}
步骤2:修改APIClient的依赖类型
将APIClient中urlSession属性的类型改为上述协议,同时保留默认初始化逻辑:
class APIClient { let urlSession: URLSessionProtocol init(urlSession: URLSessionProtocol = URLSession.shared) { self.urlSession = urlSession } typealias ResponsePublisher<T> = AnyPublisher<T, APIRequestError> // 原dispatch方法完全保留 func dispatch<T: Decodable>(request: URLRequest) -> ResponsePublisher<T> { urlSession .dataTaskPublisher(for: request) .receive(on: DispatchQueue.global(qos: .background)) .tryMap { data, response in if let response: HTTPURLResponse = response as? HTTPURLResponse, !(200...299).contains(response.statusCode) { throw APIClient.httpError(response.statusCode) } return data } .decode(type: T.self, decoder: JSONDecoder()) .mapError { APIClient.handleNonHTTPError($0) } .eraseToAnyPublisher() } // 保留原有错误处理方法 static func httpError(_ statusCode: Int) -> APIRequestError { // 你的错误实现逻辑 } static func handleNonHTTPError(_ error: Error) -> APIRequestError { // 你的错误转换逻辑 } } // 假设已定义的错误枚举 enum APIRequestError: Error { case httpStatus(Int) case decodingError case networkError(Error) }
步骤3:编写Mock URLSession实现
创建遵守URLSessionProtocol的Mock类,通过配置模拟的data、response、error来控制返回结果:
class MockURLSession: URLSessionProtocol { // 可配置的模拟参数 var mockData: Data? var mockResponse: URLResponse? var mockError: Error? func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher { // 利用DataTaskPublisher的初始化方法,直接传入模拟值 return URLSession.DataTaskPublisher( request: request, session: URLSession.shared, data: mockData, response: mockResponse, error: mockError ) } }
步骤4:编写单元测试用例
针对不同场景(成功、HTTP错误、解码错误等)编写测试:
测试成功场景
import XCTest import Combine class APIClientTests: XCTestCase { var cancellables = Set<AnyCancellable>() func testDispatch_SuccessResponse() { // 1. 准备测试数据 let mockData = #"{"id":1,"name":"TestItem"}"#.data(using: .utf8)! let mockResponse = HTTPURLResponse( url: URL(string: "https://test.com")!, statusCode: 200, httpVersion: nil, headerFields: nil )! // 2. 配置MockSession let mockSession = MockURLSession() mockSession.mockData = mockData mockSession.mockResponse = mockResponse // 3. 初始化APIClient并发起请求 let apiClient = APIClient(urlSession: mockSession) let expectation = self.expectation(description: "Receive success response") let testRequest = URLRequest(url: URL(string: "https://test.com")!) apiClient.dispatch(request: testRequest) .sink(receiveCompletion: { completion in if case .failure(let error) = completion { XCTFail("Expected success, got error: \(error)") } expectation.fulfill() }, receiveValue: { (model: TestModel) in XCTAssertEqual(model.id, 1) XCTAssertEqual(model.name, "TestItem") }) .store(in: &cancellables) waitForExpectations(timeout: 1) } // 测试用Model struct TestModel: Decodable { let id: Int let name: String } }
测试HTTP错误场景
func testDispatch_HTTPError() { let mockResponse = HTTPURLResponse( url: URL(string: "https://test.com")!, statusCode: 404, httpVersion: nil, headerFields: nil )! let mockSession = MockURLSession() mockSession.mockData = Data() mockSession.mockResponse = mockResponse let apiClient = APIClient(urlSession: mockSession) let expectation = self.expectation(description: "Receive HTTP error") let testRequest = URLRequest(url: URL(string: "https://test.com")!) apiClient.dispatch(request: testRequest) .sink(receiveCompletion: { completion in if case .failure(let error) = completion { XCTAssertEqual(error, APIRequestError.httpStatus(404)) } else { XCTFail("Expected HTTP error, got success") } expectation.fulfill() }, receiveValue: { _ in XCTFail("Should not receive value on HTTP error") }) .store(in: &cancellables) waitForExpectations(timeout: 1) }
测试解码错误场景
func testDispatch_DecodingError() { // 传入不符合Model格式的数据 let mockData = #"{"invalidKey":"value"}"#.data(using: .utf8)! let mockResponse = HTTPURLResponse( url: URL(string: "https://test.com")!, statusCode: 200, httpVersion: nil, headerFields: nil )! let mockSession = MockURLSession() mockSession.mockData = mockData mockSession.mockResponse = mockResponse let apiClient = APIClient(urlSession: mockSession) let expectation = self.expectation(description: "Receive decoding error") let testRequest = URLRequest(url: URL(string: "https://test.com")!) apiClient.dispatch(request: testRequest) .sink(receiveCompletion: { completion in if case .failure(let error) = completion { XCTAssertEqual(error, APIRequestError.decodingError) } else { XCTFail("Expected decoding error, got success") } expectation.fulfill() }, receiveValue: { _ in XCTFail("Should not receive value on decoding error") }) .store(in: &cancellables) waitForExpectations(timeout: 1) }
额外建议
- 可扩展MockURLSession,支持按不同Request返回不同模拟数据,提升测试灵活性
- 注意
receive(on:)带来的线程切换,确保测试用expectation正确等待异步操作完成 - 覆盖所有错误转换分支,保证错误处理逻辑的正确性
内容的提问来源于stack exchange,提问作者MattCodes
相关产品推荐
相关产品推荐

