🧪
URLProtocol を使って URLSession の stub を作る
API サーバのクライアントを書いているとき、以下のように URLSession を使用してクライアントコードを作ることがあるかと思います。(説明のためにだいぶ簡略化してますが。)
public struct FooAPIClient {
let baseURL: String
let session: URLSession
public init(
baseURL: String,
session: URLSession = URLSession.shared
) {
self.baseURL = baseURL
self.session = session
}
public func execute(with request: URLRequest) async throws -> T {
return try await withCheckedThrowingContinuation { continuation in
let task = session.dataTask(with: request) { data, response, error in
let result = .... // (省略) JSON 受け取って struct にしたり。
continuation.resume(returning: result)
}
task.resume()
}
}
}
この API クライアントのテストを書きたい場合、session
引数として与えられる URLSession を抽象化した Protocol を作る方法でも可能ではありますが、 URLProtocol という仕組みを使うことで実装コードには変更を加えずに HTTP 通信をスタブすることが可能です。
作り方は簡単で、 URLProtocol を継承しいくつかのメソッドを override するだけです。
typealias RequestHandler = ((URLRequest) throws -> (HTTPURLResponse, Data?))
class StubURLProtocol: URLProtocol {
static var requestHandler: RequestHandler?
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 handler = Self.requestHandler else {
fatalError("Handler is unavailable")
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
if let data = data {
client?.urlProtocol(self, didLoad: data)
}
client?.urlProtocolDidFinishLoading(self)
} catch let e {
client?.urlProtocol(self, didFailWithError: e)
}
}
override func stopLoading() {}
}
テスト中での使い方。
func testURLSessionStub() async throws {
// sample.json の中身をレスポンスとして返すようにしている
StubURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(
url: request.url!, statusCode: 200, httpVersion: "HTTP/2", headerFields: [:])!
guard
let jsonDataURL = Bundle.module.url(forResource: "sample", withExtension: "json"),
let jsonData = try? String(contentsOf: jsonDataURL)
else {
fatalError("sample.json not found")
}
let data = jsonData.data(using: .utf8)
return (response, data)
}
// URLSession を作る
let config = URLSessionConfiguration.default
config.protocolClasses = [StubURLProtocol.self]
let session = URLSession(configuration: config)
// APIClient を作る
let client = FooAPIClient(baseURL: "http://localhost:3000/", session: session)
// request 投げる
let request = URLRequest(url: URL(string: "https://example.com/")!)
let response = try await client.execute(with: request)
// response に対して何かテスト
dump(response)
}
参考
Discussion