I'm using a custom URLProtocol
for mocking server communication in unit tests:
override func startLoading() {
let response: Result<Data, Error>
if self.request.url == failureTransferURL {
response = .failure(MockError())
} else {
response = .success(mockTransferData)
}
switch response {
case let .success(data):
// Step 2: Split data into chunks
let chunkSize = 1024
self.chunksRemaining = stride(from: 0, to: data.count, by: chunkSize).map {
data[$0 ..< min($0 + chunkSize, data.count)]
}
self.chunkSendInterval = Self.transferDuration / TimeInterval(self.chunksRemaining.count)
// Simulate response on a background thread.
workerQueue.async {
// Step 1: Simulate receiving an URLResponse. We need to do this
// to let the client know the expected length of the data.
let response = URLResponse(
url: self.request.url!,
mimeType: nil,
expectedContentLength: data.count,
textEncodingName: nil
)
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
self.sendNextChunk()
}
case let .failure(error):
// Simulate error.
workerQueue.async {
self.client?.urlProtocol(self, didFailWithError: error)
}
}
}
@objc
private func sendNextChunk() {
// Simulate response on a background thread.
workerQueue.asyncAfter(deadline: .now() + self.chunkSendInterval) {
guard !self.wasStopped else {
self.client?.urlProtocol(self, didFailWithError: CancellationError())
return
}
guard !self.chunksRemaining.isEmpty else {
// Step 4: Finish loading (required).
self.client?.urlProtocolDidFinishLoading(self)
return
}
let chunk = self.chunksRemaining.removeFirst()
CopresenceStudio.log.info("Sent Chunk")
self.client?.urlProtocol(self, didLoad: chunk)
self.sendNextChunk()
}
}
override func stopLoading() {
// Required by the superclass.
wasStopped = true
}
This works fine for downloads, and I get progress updates via the URLSessionDownloadDelegate
.
However, when I create a session.uploadTask(...)
and set the delegate, the 'progress' delegate method does not fire:
extension NetworkUploadMockTests: URLSessionTaskDelegate {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64
) {
// DOES NOT FIRE
print("Sent \(totalBytesSent) / \(totalBytesExpectedToSend) Bytes.")
self.uploadProgresses?.fulfill()
}
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?
) {
// DOES FIRE
if error != nil {
self.uploadWasCancelled?.fulfill()
} else {
self.uploadFinishes?.fulfill()
}
}
}
The unit test is pretty simple:
func testBasicUploading() throws {
self.uploadFinishes = self.expectation(description: "Fake Upload succeeds")
self.uploadProgresses = self.expectation(description: "Upload Progresses")
self.uploadProgresses?.assertForOverFulfill = false
self.urlSession = URLSession(
configuration: .testing(transferDuration: 2.0), // just sets the custom protocols and how long it takes to load the data
delegate: self,
delegateQueue: nil
)
let urlRequest = URLRequest(url: uploadURL)
let data = mockTransferData
let task = urlSession!.uploadTask(with: urlRequest, from: data)
task.delegate = self
task.resume()
self.wait(for: [self.uploadFinishes!, self.uploadProgresses!], timeout: 2.5)
}
I can provide more code if necessary, but I guess it's just about knowing whether I made a false assumption about URLProtocol
subclasses and what they can do; obviously all good for downloads, but are they able to fake the uploads as well?