Categories
iOS Swift

Reading GraphQL queries from URLRequest in network tests

Back in 2019 I wrote about Testing networking code with custom URLProtocol on iOS. Recently, I was using the same approach for setting up network mocking for GraphQL requests, but then I stumbled on something unexpected. If we create an URLRequest which has httpBody set, then inside the custom URLProtocol implementation, httpBody method returns nil, and we need to access httpBodyStream instead. When dealing with GraphQL requests, the GraphQL query is part of the data set to httpBody. Reading the data from httpBody property would be straight-forward, then httpBodyStream on the other hand returns an InputStream and this requires a bit of code. InputStreams can only be read once, therefore while inspecting the URLRequest inside the URL protocol, we need to make sure not to read it twice. This is a nature of how input streams work.

In the snippet below, we can see a Data extension which adds a new initializer which takes in an InputStream. The implementation opens the stream, closes it when initializer is exited, reads data 512 bytes at the time. The read function of the InputStream returns number of read bytes if successful, negative value on error and 0 when buffer end has been reached which means that there is noting more to read.

extension Data {
init(inputStream: InputStream) throws {
inputStream.open()
defer { inputStream.close() }
self.init()
let bufferSize = 512
var readBuffer = [UInt8](repeating: 0, count: bufferSize)
while inputStream.hasBytesAvailable {
let readBytes = inputStream.read(&readBuffer, maxLength: bufferSize)
switch readBytes {
case 0:
break
case ..<0:
throw inputStream.streamError!
default:
append(readBuffer, count: readBytes)
}
}
}
}

Let’s see how to use this extension and reading a GraphQL query from the URLRequest. In the example below we can see that the example GraphQL data is set to a dictionary with 3 keys: query, operationName, and variables. Therefore, we need to first turn the InputStream into Data and then decoding the data to a model type and after that reading the query. Since we know the query, we can proceed with writing network tests where the mocked response depends on the query.

// Example data set to URLRequest httpBody
// {
// "query": "query HeroNameAndFriends($episode: Episode) { hero(episode: $episode) { name friends { name } } }",
// "operationName": "",
// "variables": { "episode": "JEDI" }
// }
struct Payload: Decodable {
let query: String
}
let outputData = try Data(inputStream: httpBodyStream)
let payload = try JSONDecoder().decode(Payload.self, from: outputData)
print(payload.query)
// Prints: query HeroNameAndFriends($episode: Episode) { hero(episode: $episode) { name friends { name } } }
view raw Usage.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.