[Android] OkHttpで通信を遅延・タイムアウト再現する方法

スポンサーリンク
  1. 導入:通信が遅い・タイムアウト…「再現できない」が一番困る
  2. 基礎:OkHttpのタイムアウト種類を整理(ここがズレると全滅)
  3. 最短で動く:OkHttpのタイムアウト設定(検証用プロファイル)
    1. Gradle(OkHttp)
    2. OkHttpClient(検証用)
  4. 方法1:MockWebServerで「遅延/無応答/切断」を作る(いちばん実務的)
    1. Gradle(MockWebServer)
    2. ケースA:レスポンスを“わざと遅らせる”(readTimeout検証)
    3. ケースB:ボディを“ゆっくり送る”(遅い回線っぽい)
    4. ケースC:無応答/切断を作る(タイムアウト・IOException検証)
  5. 方法2:Interceptorで“アプリ側だけ”遅くする(軽い・ただし注意)
    1. 遅延Interceptor(Debugビルド限定推奨)
    2. 組み込み
  6. 方法3:端末/エミュレータ側で回線を遅くする(実機っぽい検証)
  7. 実務での落とし穴・アンチパターン
    1. 落とし穴1:タイムアウトの種類を間違えて検証してる
    2. 落とし穴2:Interceptor遅延で“本番の遅さ”を再現した気になる
    3. 落とし穴3:リトライ設定がテストを壊す
    4. 落とし穴4:キャンセルを検証していない
    5. 落とし穴5:UIスレッドをブロックして検証がズレる
  8. テスト観点まとめ:遅延/タイムアウトで何を見る?
  9. ベストプラクティス:3つを使い分ける
  10. デバッグ/確認チェックリスト
  11. よくある質問(Q&A)
    1. Q. 一番おすすめの再現方法は?
    2. Q. Interceptorの遅延は本番の遅さと同じ?
    3. Q. タイムアウトって短くすればするほど良い?
    4. Q. readTimeoutとcallTimeout、どっちを見るべき?
    5. Q. Retrofitでも同じ?
  12. まとめ:要点3つ+迷ったらこれ

導入:通信が遅い・タイムアウト…「再現できない」が一番困る

通信まわりの不具合って、だいたいこう言われます。

  • 「たまに遅い」
  • 「たまにタイムアウトする」
  • 「電波悪いと落ちる」

で、手元はWi-Fiで爆速。再現できない。ここでハマる。

結論から言うと、“わざと”遅くする仕組みを用意しないと、検証は前に進みません。
OkHttpなら、アプリ側・テスト側・端末側の3方向から再現できます。

先に結論(3行)です。

  • 単体テスト/結合テストは MockWebServer が最強(遅延/無応答/切断を作れる)
  • アプリ内で疑似再現したいなら Interceptor(ただし本物のネットワーク遅延ではない)
  • 実機っぽくやるなら エミュレータの回線制限(速度制限が簡単)

基礎:OkHttpのタイムアウト種類を整理(ここがズレると全滅)

OkHttpのタイムアウトは複数あります。雑に「タイムアウト」と言うと、検証も雑になります。

  • connectTimeout:接続確立まで待つ時間
  • readTimeout:受信待ち(サーバからデータが来ない)
  • writeTimeout:送信待ち(リクエスト送れない)
  • callTimeout:1リクエスト全体の上限(開始〜完了まで)

「遅い」を再現したいのか、「無応答」を再現したいのかで、狙うべきタイムアウトが変わります。
OkHttpのBuilderでも各タイムアウトは明確に分かれています。

最短で動く:OkHttpのタイムアウト設定(検証用プロファイル)

まずは検証用にタイムアウト値を短めにしたクライアントを作ります。
RetrofitでもOkHttp直でも同じです。

Gradle(OkHttp)

dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
}

OkHttpClient(検証用)

import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit

fun buildTestClient(): OkHttpClient {
    return OkHttpClient.Builder()
        .connectTimeout(2, TimeUnit.SECONDS) // 接続が遅いケース
        .readTimeout(2, TimeUnit.SECONDS)    // 応答が遅い/止まるケース
        .writeTimeout(2, TimeUnit.SECONDS)   // 送信が詰まるケース
        .callTimeout(3, TimeUnit.SECONDS)    // 全体上限(地味に便利)
        .build()
}

ポイントは callTimeout です。
readTimeoutだけだと「ちょいちょいデータが来る」パターンで延々伸びることがあり、検証がブレます。
全体上限で“強制的に終わらせる”とテストが安定します。

方法1:MockWebServerで「遅延/無応答/切断」を作る(いちばん実務的)

テストで再現するなら、基本は MockWebServer です。
遅延や切断などの異常系を作りやすいのが強い。

Gradle(MockWebServer)

dependencies {
    testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
    testImplementation(kotlin("test"))
}

ケースA:レスポンスを“わざと遅らせる”(readTimeout検証)

import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import java.util.concurrent.TimeUnit
import kotlin.test.Test
import kotlin.test.assertFails

class SlowResponseTest {

    @Test
    fun readTimeout_shouldFail_whenResponseIsDelayed() {
        val server = MockWebServer().apply { start() }

        // 3秒待ってから返す(readTimeout 2秒なら落ちる)
        server.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setBody("OK")
                .setBodyDelay(3, TimeUnit.SECONDS) // レスポンス開始を遅らせる
        )

        val client = OkHttpClient.Builder()
            .readTimeout(2, TimeUnit.SECONDS)
            .build()

        val url = server.url("/slow")
        val request = Request.Builder().url(url).build()

        assertFails {
            client.newCall(request).execute().use { /* no-op */ }
        }

        server.shutdown()
    }
}

これで「遅延」を確実に再現できます。
MockWebServerの遅延設定は、タイムアウト検証で定番です。

ケースB:ボディを“ゆっくり送る”(遅い回線っぽい)

「レスポンス開始は早いけど、ダウンロードが遅い」を作りたいことがあります。
その場合はスロットリングが便利です(名前は環境/バージョンで差があるので、使っているMockWebServerのAPIを確認してください)。

server.enqueue(
    MockResponse()
        .setResponseCode(200)
        .setBody("x".repeat(1024 * 1024)) // 1MB
        // throttleBody / setThrottleBody 等が使える場合はここで絞る
)

ここは「遅い=readTimeout」だけじゃなく、UI側のプログレス表示やキャンセル挙動の検証に効きます。

ケースC:無応答/切断を作る(タイムアウト・IOException検証)

本番で起きるのは「遅延」だけじゃないです。
サーバが落ちる、途中で切れる、最初から切れる。こういう地獄も再現したい。

import okhttp3.mockwebserver.SocketPolicy

server.enqueue(
    MockResponse()
        .setSocketPolicy(SocketPolicy.NO_RESPONSE) // 返さない(タイムアウト狙い)
)

server.enqueue(
    MockResponse()
        .setSocketPolicy(SocketPolicy.DISCONNECT_AT_START) // 最初から切断(IOException狙い)
)

SocketPolicy には切断系がいろいろあります。切断の再現はMockWebServerが強いです。

方法2:Interceptorで“アプリ側だけ”遅くする(軽い・ただし注意)

「アプリ側で簡単に遅延を入れて、UIを確認したい」なら Interceptor が手軽です。

ただし、これは本物のネットワーク遅延ではありません
DNSやTCP、TLS、電波状況の影響は再現できない。
それでも「ローディング表示」「キャンセル」「二重タップ防止」みたいなUI検証には十分役立ちます。

遅延Interceptor(Debugビルド限定推奨)

import okhttp3.Interceptor
import okhttp3.Response

class ArtificialDelayInterceptor(
    private val delayMs: Long
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        // わざと遅らせる(疑似的な回線遅延)
        Thread.sleep(delayMs)
        return chain.proceed(chain.request())
    }
}

組み込み

val client = OkHttpClient.Builder()
    .addNetworkInterceptor(ArtificialDelayInterceptor(delayMs = 1500))
    .build()

Debug限定にしてください。Releaseに入れると事故ります。

方法3:端末/エミュレータ側で回線を遅くする(実機っぽい検証)

「本物の遅さ」を見るなら、端末側で絞るのが一番それっぽいです。
Android Emulator には回線速度の設定があり、Extended controls から変更できます。

  • 通信速度を絞る(Down/Up)
  • 条件を変えて挙動を見る(Wi-Fi→セルラーの雰囲気)

ただ、環境によっては「速度は絞れるけど遅延(latency)までは細かく触れない」など限界もあります。
なので、エミュレータ=体感確認MockWebServer=再現性のあるテスト、みたいに使い分けるのが現実的です。

実務での落とし穴・アンチパターン

落とし穴1:タイムアウトの種類を間違えて検証してる

connectTimeout を検証したいのに readTimeout だけいじる、みたいなズレはよくあります。
「何が遅い想定か」を決めて、狙うタイムアウトを合わせてください。

落とし穴2:Interceptor遅延で“本番の遅さ”を再現した気になる

Interceptor遅延は便利ですが、ネットワーク層の遅延とは別物です。
DNS/TLS/再送/輻輳は再現できません。
「UIの待ち」を見る道具として割り切るのが正解です。

落とし穴3:リトライ設定がテストを壊す

OkHttpや上位レイヤー(Retrofit + 独自実装など)でリトライしていると、
タイムアウトのつもりが「数回やり直して長引く」になりがちです。
テストではリトライをOFFにするか、回数を固定してください。

落とし穴4:キャンセルを検証していない

遅延を入れたら、次に見るべきはキャンセルです。
画面を閉じたのに通信が残り続けると、地味にメモリも電池も食います。

落とし穴5:UIスレッドをブロックして検証がズレる

UI検証で遅延を入れる時に、メインスレッドで寝かせると別の問題になります。
「UIが固まった」になるので、遅延の入れどころは注意です。

テスト観点まとめ:遅延/タイムアウトで何を見る?

  • ローディング表示(出る/消える/二重表示しない)
  • キャンセル(画面離脱で止まる、再入場で二重リクエストしない)
  • リトライ方針(回数、間隔、ユーザー通知)
  • タイムアウト時のエラー文言(ユーザーに伝わるか)
  • 再試行ボタンの挙動(連打で増殖しない)
  • オフライン復帰(通信復帰で自然に回復するか)

ベストプラクティス:3つを使い分ける

迷ったら、この使い分けが一番安定します。

  • MockWebServer:再現性のある自動テスト(遅延/無応答/切断)
  • Interceptor:UIの待ち・キャンセル検証(簡易)
  • エミュレータ回線制限:体感確認(速度を落として現場っぽく)

デバッグ/確認チェックリスト

  • どのタイムアウト(connect/read/write/call)を検証したいか決めた
  • MockWebServerで遅延/無応答/切断を再現できている
  • UIはローディング・キャンセル・リトライを確認した
  • Interceptor遅延はDebug限定で入れている
  • リトライがテストを壊していない

よくある質問(Q&A)

Q. 一番おすすめの再現方法は?

自動テストとして安定するのは MockWebServer です。遅延や無応答、切断が作れて、再現性が高いです。

Q. Interceptorの遅延は本番の遅さと同じ?

同じではないです。ネットワーク層の遅延(DNS/TLS/再送など)は再現できません。
ただ、UIの待ちやキャンセル確認には十分使えます。

Q. タイムアウトって短くすればするほど良い?

短すぎると、通信が不安定な環境で正当な失敗が増えます。
アプリの用途(一覧取得/決済/アップロード)で許容時間は変わるので、機能単位で設計するのが現実的です。

Q. readTimeoutとcallTimeout、どっちを見るべき?

目的次第です。readTimeoutは「受信が止まる」検知、callTimeoutは「全体の上限」でテストを安定させたい時に便利です。
両方使うとブレが減ります。

Q. Retrofitでも同じ?

OkHttpの層で起きているので同じです。Retrofitは内部でOkHttpを使うので、OkHttpClientの設定と再現手法がそのまま効きます。

まとめ:要点3つ+迷ったらこれ

  • 遅延/タイムアウト検証は「再現装置」がないと始まらない
  • MockWebServerで遅延・無応答・切断まで再現できる
  • Interceptorと端末側制限は用途を割り切って使う

迷ったらこれ:自動テストはMockWebServer、UI確認はInterceptor、体感はエミュレータ回線制限。

コメント

タイトルとURLをコピーしました