Hello,
I've started using URLSessionWebSocketTask to perform very simple socket operations.
To avoid needing an external connection while testing I've opted to create a URLProtocol
(which I've used previously for Data tasks), but it's unclear if this is the direction I should be taking.
Within the startLoading()
method I'm properly creating the hand-shake response according to RFC6455 (previously I was getting a Bad Response error).
override open func startLoading() {
let requestKey = request.allHTTPHeaderFields!["Sec-WebSocket-Key"]!
let sha1HexString = "\(requestKey)258EAFA5-E914-47DA-95CA-C5AB0DC85B11".sha1()
let acceptHeaderValue = Data(
fromHexEncodedString: sha1HexString
)!.base64EncodedString()
let response = HTTPURLResponse(
url: url,
statusCode: 101,
httpVersion: "http/1.1",
headerFields: [
"Sec-WebSocket-Accept": acceptHeaderValue,
"upgrade": "websocket",
"connection": "upgrade"
])!
self.client.urlProtocol(
self, didReceive: response, cacheStoragePolicy: .notAllowed
)
/* `data` is generated elsewhere and doesn't seem relevant*/
self.client.urlProtocol(self, didLoad: data)
self.client.urlProtocolDidFinishLoading(self)
}
I've changed the order of calling didReceive:
and didLoad
, but everything results in the following two errors in my logs:
- Connection not set before response is received, failing task
- Task <SessionID>.<TaskId> finished with error [-1005] Error Domain=NSURLErrorDomain Code=-1005 "The network connection was lost." UserInfo={NSErrorFailingURLStringKey=ws://localhost:8080, NSErrorFailingURLKey=ws://localhost:8080, _NSURLErrorRelatedURLSessionTaskErrorKey=("LocalWebSocketTask <SessionID>.<TaskId>"), _NSURLErrorFailingURLSessionTaskErrorKey=LocalWebSocketTask <SessionID>.<TaskId>, NSLocalizedDescription=The network connection was lost.}
I've set breakpoints within startLoading
, so I know it's using my custom URLProtocol
, but I can't seem to figure out how to make it work.
Is it possible to use URLProtocol with sockets, or is there an alternative approach to mocking WebSockets? I want to avoid wrapping everything within layers of protocols, but completely replacing URLSession seems like the only course forward.
Thank you!
To avoid needing an external connection while testing I've opted to create a
URLProtocol
I’m not a big fan of custom URLProtocol
subclasses, and I literally wrote the ‘book’ on them (see CustomHTTPProtocol sample code). Hmmm, maybe I’m not a big fan of them because I did that (-:
As I explain in the sample’s read me, URLProtocol
dates from the NSURLConnection
era and hasn't been updated to handle the stuff that was added in the first iteration of URLSession
. It certainly can’t handle anything more modern, like web socket tasks.
The best way to test web socket tasks is to run them against a local server. Network framework has direct support for creating a WebSocket server, so this is relatively straightforward. For an example, see this post [1].
I apply the same logic for HTTP as well, although HTTP is harder because there’s no built-in HTTP server. In that case I’d probably build a test server on top of Swift NIO, or something layered on top of that, like Vapour Vapor.
Still, none of this is unit testing. In networking code there are usually three items in play:
-
Your network logic. This tracks the state of the connections, renders outgoing messages to bytes, parses incoming bytes as messages, and so on.
-
Your interaction with the network. This accepts outgoing bytes from the above, passes incoming bytes to the above, and also generates other events necessary to manage its state.
-
The network itself.
You can unit test the first item but you don’t need to mock the networking API to do it. That’s because there’s a natural interface between the first item and the second, and to test the first item you create a test implementation of the second. That implementation doesn’t have to be a mock of, say, URLSession
because you get to define the interface.
When you go to test the second item you’re not creating a unit test because your implementation is tightly bound to the networking API’s implementation, and that means you’re creating an integration test. The goal of the test is to confirm that the state transitions you get from the networking API are handled correctly by your code. If you then mock the networking API you’re unlikely to uncover any bugs because your mock will behave like you think the networking API behaves, and your code is already designed to handle that. What you want to test is how the networking API actually behaves, and for that you have to hit the network.
I usually do that with a loopback server, as discussed above.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] While you’re there, think about whether you want to continue using URLSession
web socket tasks. IMO NWConnection
is the nicer API for both client and server side.