如何用URLProtocolStub测试Alamofire的Multipart参数上传
问题:Alamofire Multipart请求测试中无法获取httpBody内容
背景
我想用Alamofire测试Multipart参数的发送(同时发送字符串和Data类型图片),但拦截到的URLRequest的httpBody始终为nil,不知道如何获取Multipart数据做验证。已做过相关搜索,目前用URLProtocolStub模拟会话,相关代码如下:
URLProtocolStub实现
final class URLProtocolStub: URLProtocol { private struct Stub { let data: Data? let response: URLResponse? let error: Error? let requestObserver: ((URLRequest) -> Void)? } private static var _stub: Stub? private static var stub: Stub? { get { return queue.sync { _stub } } set { queue.sync { _stub = newValue } } } private static let queue = DispatchQueue(label: "URLProtocolStub.queue") static func stub(data: Data?, response: URLResponse?, error: Error?) { stub = Stub(data: data, response: response, error: error, requestObserver: nil) } static func observeRequests(observer: @escaping (URLRequest) -> Void) { stub = Stub(data: nil, response: nil, error: nil, requestObserver: observer) } override class func canInit(with request: URLRequest) -> Bool { return true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } override func startLoading() { guard let stub = URLProtocolStub.stub else { return } if let data = stub.data { client?.urlProtocol(self, didLoad: data) } if let response = stub.response { client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) } if let error = stub.error { client?.urlProtocol(self, didFailWithError: error) } else { client?.urlProtocolDidFinishLoading(self) } stub.requestObserver?(request) } }
配置Alamofire模拟会话
let configuration = URLSessionConfiguration.af.ephemeral configuration.protocolClasses = [URLProtocolStub.self]
测试函数
func test_uploadMultipart_with_params() async { // 1 let httpURL = HTTPURLResponse(statusCode: 204) let bodyParams = ["firstname": "mock", "lastname": "test"] let imageData = UIColor.yellow.image(CGSize(width: 128, height: 128)).jpegData(compressionQuality: 0.7) URLProtocolStub.stub(data: nil, response: httpURL, error: nil) let exp = expectation(description: "Waiting to receive response") // 2 let loggerDelegate = StubHTTPLoggerDelegate(didReceivedResponse: { request in XCTAssertEqual(request?.httpBody.flatMap { String(data: $0, encoding: .utf8) }, "firstname=mock&lastname=test") exp.fulfill() }) // 3 let result = await makeSUT(loggerDelegate: loggerDelegate).requestMultipart(imageDatas: [imageData], pathType: anyPathType(.post, bodyParameters: bodyParams, urlParameters: nil)) do { try result.get() } catch { XCTFail("\(error)") } wait(for: [exp], timeout: 1.0) }
测试步骤:
- 准备Multipart请求的字符串参数和图片数据
- 创建监听器接收响应,尝试通过
httpBody验证参数 - 创建Alamofire客户端并执行Multipart请求
问题核心:测试失败,因为拦截到的URLRequest的httpBody为nil,不知道如何获取Multipart数据进行验证。
解决方案
原因分析
Alamofire发送Multipart请求时,不会直接把数据放到URLRequest.httpBody中,而是通过URLSessionTask的httpBodyStream流式上传数据,这就是你拿到httpBody为nil的根本原因。
解决方法:从httpBodyStream读取数据
修改URLProtocolStub的startLoading方法,读取httpBodyStream的内容并生成带完整body的请求副本,再传给观察者:
1. 扩展InputStream读取全部数据
extension InputStream { func readAllData() -> Data { open() let bufferSize = 1024 var buffer = [UInt8](repeating: 0, count: bufferSize) var data = Data() while hasBytesAvailable { let bytesRead = read(&buffer, maxLength: bufferSize) if bytesRead > 0 { data.append(buffer, count: bytesRead) } } close() return data } }
2. 修改URLProtocolStub的startLoading方法
override func startLoading() { guard let stub = URLProtocolStub.stub else { return } // 处理请求的body stream,生成带完整httpBody的请求 let requestToObserve: URLRequest if let stream = request.httpBodyStream, let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest { let bodyData = stream.readAllData() mutableRequest.httpBody = bodyData requestToObserve = mutableRequest as URLRequest } else { requestToObserve = request } // 原有响应逻辑不变 if let data = stub.data { client?.urlProtocol(self, didLoad: data) } if let response = stub.response { client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) } if let error = stub.error { client?.urlProtocol(self, didFailWithError: error) } else { client?.urlProtocolDidFinishLoading(self) } // 传入处理后的请求给观察者 stub.requestObserver?(requestToObserve) }
验证Multipart内容
现在可以在测试的观察者里拿到完整的Multipart数据,需要解析boundary分隔的各个part来验证参数,而不是直接对比表单字符串:
let loggerDelegate = StubHTTPLoggerDelegate(didReceivedResponse: { request in guard let bodyData = request?.httpBody, let bodyString = String(data: bodyData, encoding: .utf8) else { XCTFail("No body data found") exp.fulfill() return } // 验证字符串参数存在 XCTAssertTrue(bodyString.contains("firstname=mock")) XCTAssertTrue(bodyString.contains("lastname=test")) // 验证图片相关的part存在 guard let contentType = request?.value(forHTTPHeaderField: "Content-Type"), let boundaryRange = contentType.range(of: "boundary=") else { XCTFail("Invalid multipart content type") exp.fulfill() return } // 检查是否包含图片的Content-Disposition和类型(替换成你实际的图片参数名) XCTAssertTrue(bodyString.contains("Content-Disposition: form-data; name=\"image\"")) XCTAssertTrue(bodyString.contains("image/jpeg")) exp.fulfill() })
注意事项
- Multipart数据由boundary分隔多个独立part,每个part包含参数名、内容类型和数据,不能直接和
firstname=mock&lastname=test这类表单字符串对比 - 确保你的
requestMultipart方法正确构建了Multipart表单,包含所有需要的参数和图片数据
内容的提问来源于stack exchange,提问作者Tharsan E.
相关产品推荐
相关产品推荐

