URLSessionのモックのしかた
Table of Contents
ユニットテストや結合テストをする際、URLSessionをモックすることがあります。 便利なライブラリ(例: Mockingjay)もありますが、今回は自分で実装してみます。 思っていた以上にかんたんだったので、今日はその方法をまとめます。
参考にするサイト #
- How to test iOS networking code the easy way – Hacking with Swift
- URLProtocol を使って URLSession の stub を作る
- URLSession: Stubbing Network Responses for Unit Tests | by TheiOSDude | Medium
下準備 #
URLSessionを使用するクラスを、予めモックを差し込めるようにしておくと良いです。
// before
final class ApiClient {
func send(_ request: URLRequest) {
_ = try! await URLSession.shared.data(for: request)
}
}
// after
final class ApiClient {
let session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
func send(_ request: URLRequest) {
_ = try! await session.data(for: request)
}
}
こうしておくことで、モック済みのURLSessionを外部から挿し込めるようになります。
手順 #
では、モックを実装していきます。URLProtocol
を継承したクラスを実装します。
URLProtocolのサブクラスを実装する #
ここから4つのメソッドをオーバーライドしていきます。
canInit(with:)
- URLRequestを引数に受ける
- すべてのリクエストをモックする場合は、
true
を返すだけでよい - 特定のリクエストだけモックしたい場合は、ここで、想定しているURLかどうかなどを判定したBoolを返す
canonicalRequest(for:)
- URLRequestを引数に受ける
- 引数で受けたURLRequestを、そのまま返すだけで良い
- キャッシュにヒットさせるなどを目的があれば、リクエストを正規化する
startLoading()
- リクエストに応じて、モックしたレスポンスを返すように実装する
URLProtocolClient
を通じて、URLローディングシステムにフィードバックを返す- フィードバックに使用するメソッドは、URLProtocolclientに記載されている
stopLoading()
- モックが目的であれば何も実装する必要はないが、メソッドは用意しておく必要がある
- ドキュメントによれば、システムからこのメソッドが呼び出されたら、ローディングを停止する必要があるとのこと
final class URLProtocolMock: URLProtocol {
static var testURLs = [URL?: Data]()
override class func canInit(with request: URLRequest) -> Bool {
// 全リクエストをモックする
true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
// リクエストは正規化せずそのまま
request
}
override func startLoading() {
if
let url = request.url, // requestはInstance Property
let data = URLProtocolMock.testURLs[url]
{
// レスポンスを作って
let response = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: "HTTP/2",
headerFields: nil
)!
// レスポンスを受信したことをクライアントに通知
client?.urlProtocol(
self,
didReceive: response,
cacheStoragePolicy: .notAllowed
)
// データをクライアントに通知
client?.urlProtocol(
self,
didLoad: data
)
}
// ローディングの終了を通知
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {
// 実装の必要なし
}
}
URLProtocolclientでは、didLoad
の他、RedirectやAuthenticationChallengeも通知できるようになっています。細かく挙動をモックできるようになっています。
通信をモックする #
先程実装したURLProtocolのサブクラスを、URLSessionで使えるようにします。 URLSessionConfigurationを経由して渡します。
// モックするURLとレスポンスを登録する
URLProtocolMock.testUrls = [
URL(string: "https://example.com/a"): "Response for A".data(using: .utf8)!,
URL(string: "https://example.com/b"): "Response for B".data(using: .utf8)!
]
// モックをConfigに登録
let config = URLSessionConfiguration.ephemeral // ephemeralでなく、defaultでもよい
config.protocolClasses = [URLProtocolMock.self]
// URLProtocolMockを組み込んだURLSessionを作る
let session = URLSession(configuration: config)
// ApiClientにモックしたsessionを挿し込めばOK
let apiClient = ApiClient(session: session)
たったこれだけでOK。
ここで挙げたURLProtocol
のモックは、レスポンスをモックするテストであれば十分なものです。
ステータスコードを200以外にすることもできますし、データもカスタマイズできます。
もし、エラーが投げられた際のテストをしたい場合は、最初に上げた参考資料にもエラーを含むモックがあるので、参考にしてみてください。