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

如何用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.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 04:55:08