Skip to main content
  1. Posts/

URLSessionのモックのしかた

ユニットテストや結合テストをする際、URLSessionをモックすることがあります。 便利なライブラリ(例: Mockingjay)もありますが、今回は自分で実装してみます。 思っていた以上にかんたんだったので、今日はその方法をまとめます。

参考にするサイト #

下準備 #

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以外にすることもできますし、データもカスタマイズできます。 もし、エラーが投げられた際のテストをしたい場合は、最初に上げた参考資料にもエラーを含むモックがあるので、参考にしてみてください。