ユーザ用ツール

サイト用ツール


サイドバー

プログレス合同会社

広告

android:studio:application:okhttp-android-test

23.OkHttpのエミュレータテスト

エミュレータに接続して、OkHttpの実装リポジトリOkHttpHtmlRepositoryImplのテストを行います。
実際に外部サイト(テスト用サイト)に接続してのテストになります。

エミュレータテスト用モジュールの参照

エミュレータテストに必要なライブラリを:core:dataモジュールのbuild.gradle.ktsに追記します。

   :
android {
   :
  defaultConfig {
    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
  }
}

dependencies {
   :
  // エミュレータ接続テスト (src/androidTest)
  androidTestImplementation(libs.bundles.androidx.test)
  androidTestImplementation(platform(libs.junit5.bom))
  androidTestImplementation(libs.bundles.junit5)
  androidTestRuntimeOnly(libs.junit5.engine)
  androidTestImplementation(libs.kotlinx.coroutines.test)
}

4行目~6行目
Instrumentation Test(androidTest)のテストランナーおよびランナービルダーを追記します。
11行目~16行目
エミュレータ用のテストモジュールを参照します。
必ずandroidTestImplementationにします

追記後、『同期アイコン』で内容をプロジェクトに反映させます。

AndroidManifestの設定

AndroidManifestで外部との通信(インターネット接続)を許可します。

   :
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>

3行目
インターネットアクセスを許可します。
4行目
ネットワーク接続状態の確認を許可します。

AndroidJUnit5Extensionの作成

JUnit 5スタイルで実機/エミュレータテストを行うための拡張(Extension)クラスをandroidTestに作成します。

package jp.co.example.android01.core.data

import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.jupiter.api.extension.*

class AndroidJUnit5Extension : BeforeAllCallback, BeforeEachCallback, AfterEachCallback, AfterAllCallback {

  override fun beforeAll(context: ExtensionContext) {
    val instrumentation = InstrumentationRegistry.getInstrumentation()
    Log.i("AndroidJUnit5Extension", "Instrumentation initialized: $instrumentation")
  }

  override fun beforeEach(context: ExtensionContext) {
    Log.i("AndroidJUnit5Extension", "Starting test: ${context.displayName}")
  }

  override fun afterEach(context: ExtensionContext) {
    Log.i("AndroidJUnit5Extension", "Finished test: ${context.displayName}")
  }

  override fun afterAll(context: ExtensionContext) {
    Log.i("AndroidJUnit5Extension", "All tests completed for: ${context.testClass.map { it.simpleName }.orElse("UnknownClass")}")
  }
}

10行目
Instrumentationコンテキストを初期化しています。

エミュレータテストクラス用の作成

:core:dataモジュールのandroidTestにエミュレータテスト用クラスを作成します。
ファイル名をOkHttpHtmlRepositoryImplAndroidTestにしています。

package jp.co.example.android01.core.data

import android.util.Log
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.extension.ExtendWith
import jp.co.example.android01.core.data.repository.OkHttpHtmlRepositoryImpl

/**
 * OkHttpHtmlRepositoryImplのAndroid Instrumented Test
 * 実際のOkHttpクライアントを使用してテストを実行します
 */
@ExtendWith(AndroidJUnit5Extension::class)
@DisplayName("OkHttpHtmlRepositoryImpl Android Tests")
class OkHttpHtmlRepositoryImplAndroidTest {
  private lateinit var context: Context
  private lateinit var repository: OkHttpHtmlRepositoryImpl

  @BeforeEach
  fun setUp() {
    // テスト用のContextを取得
    context = InstrumentationRegistry.getInstrumentation().targetContext
    repository = OkHttpHtmlRepositoryImpl()
    Log.i("OkHttpAndroidTest", "OkHttpHtmlRepositoryImpl initialized")
  }

  @AfterEach
  fun tearDown() {
    // クリーンアップ
    Log.i("OkHttpAndroidTest", "Test cleanup completed")
  }

  @Nested
  @DisplayName("Get HTML Tests")
  inner class GetHtmlTests {
    @Test
    @DisplayName("実際のWebサイトからHTMLコンテンツを取得 - 成功")
    fun getHtmlContent_Success() = runTest {
      // Given
      val testUrl = "https://httpbin.org/html"     // テスト用のHTMLを返すサイト
      // When
      val result = repository.getHtmlContent(testUrl)
      // Then
      logLong("=== OkHttp Real Website Test Result ===")
      if (result.isSuccess) {
        val content = result.getOrNull() ?: "(no content)"
        logLong("Content length: ${content.length}")
        logLong("Content preview: $content")
      } else {
        Log.e("OkHttpAndroidTest", "Fetch failed", result.exceptionOrNull())
      }
      logLong("=======================================")

      assertTrue(result.isSuccess, "結果は成功であるべき")
      assertNotNull(result.getOrNull(), "HTMLコンテンツはnullでない")
      assertTrue(
        result.getOrNull()?.contains("html") == true,
        "HTMLコンテンツが含まれている"
      )
      assertTrue(result.getOrNull()?.isNotEmpty() == true, "コンテンツが空でない")
    }
  }

  @Nested
  @DisplayName("Error Handling Tests")
  inner class ErrorHandlingTests {
    @Test
    @DisplayName("404エラー時 失敗を返す - エラーハンドリング")
    fun getHtmlContent_404Error() = runTest {
      // Given
      val testUrl = "https://httpbin.org/status/404"
      // When
      val result = repository.getHtmlContent(testUrl)
      // Then
      logLong("=== OkHttp 404 Error Test Result ===")
      logLong("Success: ${result.isSuccess}")
      logLong("Failure: ${result.isFailure}")
      logLong("Exception: ${result.exceptionOrNull()?.message}")
      logLong("===================================")

      assertTrue(result.isFailure, "結果は失敗であるべき")
      assertNotNull(result.exceptionOrNull(), "エラーメッセージが存在する")
      assertTrue(
        result.exceptionOrNull()?.message?.contains("HTTP Error: 404") == true,
        "HTTPエラーメッセージが含まれている"
      )
    }

    @Test
    @DisplayName("500エラー時 失敗を返す - エラーハンドリング")
    fun getHtmlContent_500Error() = runTest {
      // Given
      val testUrl = "https://httpbin.org/status/500"
      // When
      val result = repository.getHtmlContent(testUrl)
      // Then
      logLong("=== OkHttp 500 Error Test Result ===")
      logLong("Success: ${result.isSuccess}")
      logLong("Failure: ${result.isFailure}")
      logLong("Exception: ${result.exceptionOrNull()?.message}")
      logLong("===================================")

      assertTrue(result.isFailure, "結果は失敗であるべき")
      assertNotNull(result.exceptionOrNull(), "エラーメッセージが存在する")
      assertTrue(
        result.exceptionOrNull()?.message?.contains("HTTP Error: 500") == true,
        "HTTPエラーメッセージが含まれている"
      )
    }

    @Test
    @DisplayName("存在しないURL - エラーハンドリング")
    fun getHtmlContent_NonExistentUrl() = runTest {
      // Given
      val testUrl = "https://this-domain-does-not-exist-12345.com"
      // When
      val result = repository.getHtmlContent(testUrl)
      // Then
      logLong("=== OkHttp Non-existent URL Test Result ===")
      if (result.isFailure) {
        val exception = result.exceptionOrNull()
        logLong("Expected failure: ${exception?.message}")
      } else {
        logLong("Unexpected success: ${result.getOrNull()?.take(100)}")
      }
      logLong("===========================================")

      // 存在しないURLでもOkHttpは何らかのHTMLを返す可能性がある
      // 成功または失敗のいずれかが適切に処理されることを確認
      assertNotNull(result.getOrNull() ?: result.exceptionOrNull(), "結果は適切に処理されるべき")
    }

    // 無効なURL時_失敗を返す
    @Test
    @DisplayName("無効なURL - エラーハンドリング")
    fun getHtmlContent_InvalidUrl() = runTest {
      // Given
      val invalidUrl = "invalid-url"
      // When
      val result = repository.getHtmlContent(invalidUrl)
      // Then
      logLong("=== OkHttp Invalid URL Test Result ===")
      logLong("Success: ${result.isSuccess}")
      logLong("Failure: ${result.isFailure}")
      logLong("Exception: ${result.exceptionOrNull()?.message}")
      logLong("=====================================")

      assertTrue(result.isFailure, "結果は失敗であるべき")
      assertNotNull(result.exceptionOrNull(), "エラーメッセージが存在する")
    }
  }

  @Nested
  @DisplayName("Edge Case Tests")
  inner class EdgeCaseTests {
    @Test
    @DisplayName("リダイレクトURL - リダイレクトテスト")
    fun getHtmlContent_RedirectUrl() = runTest {
      // Given
      val testUrl = "https://httpbin.org/redirect/1" // 1回リダイレクト
      // When
      val result = repository.getHtmlContent(testUrl)
      // Then
      logLong("=== OkHttp Redirect Test ===")
      if (result.isSuccess) {
        val content = result.getOrNull() ?: "(no content)"
        logLong("Redirect handling successful")
        logLong("Content length: ${content.length}")
      } else {
        Log.e("OkHttpAndroidTest", "Redirect fetch failed", result.exceptionOrNull())
      }
      logLong("=============================")

      // リダイレクトは成功するか、適切にエラーハンドリングされる
      assertNotNull(result.getOrNull() ?: result.exceptionOrNull(), "リダイレクトは適切に処理されるべき")
    }
  }

  // 4000文字制限対応の分割ログ
  private fun logLong(message: String) {
    if (message.length <= 4000) {
      Log.d("OkHttpAndroidTest", message)
    } else {
      var start = 0
      val length = message.length
      while (start < length) {
        val end = (start + 4000).coerceAtMost(length)
        Log.d("OkHttpAndroidTest", message.substring(start, end))
        start = end
      }
    }
  }
}

20行目
作成したAndroidJUnit5Extensionを使用します。
26行目~32行目
テスト開始前に実行する処理を定義しています。
repositoryにテスト対象のリポジトリを設定しています。
34行目~38行目
テスト終了後に実行する処理をします。
40行目~184行目
テストを実行する処理を定義しています。
40行目~42行目
テストをグループ化しています。
61行目~67行目
テストが成功した条件を定義しています。
186行目~199行目
結果のHTMLが4000文字を超える場合、ログ出力時に途中で切捨てられないための関数を定義しています。

エミュレータテストの実行

エミュレータテストの実行は、Android Studioターミナルで行います。
エミュレータテストするクラスファイルを右クリック実行(U)でも実行できます。

※エミュレータテストの実行前にエミュレーターを起動しておいてください。

# 端末にテスト用のモジュールをインストールします
./gradlew :core:data:installDebugAndroidTest

# すべてのエミュレータテスト
& "$env:ANDROID_HOME\platform-tools\adb.exe" shell am instrument -w `
-e runnerBuilder de.mannodermaus.junit5.AndroidJUnit5Builder `
jp.co.example.android01.core.data.test/androidx.test.runner.AndroidJUnitRunner 

# OkHttpのみのエミュレータテスト
& "$env:ANDROID_HOME\platform-tools\adb.exe" shell am instrument -w `
-e runnerBuilder de.mannodermaus.junit5.AndroidJUnit5Builder `
-e class jp.co.example.android01.core.data.OkHttpHtmlRepositoryImplAndroidTest `
jp.co.example.android01.core.data.test/androidx.test.runner.AndroidJUnitRunner 

# OkHttp内のgetHtmlContent_Successのみのエミュレータテスト
& "$env:ANDROID_HOME\platform-tools\adb.exe" shell am instrument -w `
-e runnerBuilder de.mannodermaus.junit5.AndroidJUnit5Builder `
-e class jp.co.example.android01.core.data.OkHttpHtmlRepositoryImplAndroidTest#getHtmlContent_Success `
jp.co.example.android01.core.data.test/androidx.test.runner.AndroidJUnitRunner 

2行目
テストクラスや定義を含め、コードを変更したときは必ず実行します。

実行結果は、Logcat内に出力されています。

android/studio/application/okhttp-android-test.txt · 最終更新: by プログレス合同会社