32.WebViewのエミュレータテスト

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

※単にサイトからHTMLを取得するだけですので、単体テストは行いません。

テスト用クラスの作成以外の前準備はOkHttpのエミュレータテストと同じです。

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

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

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

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

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

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

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

  @Nested
  @DisplayName("Get HTML Tests")
  inner class GetHtmlTests {
    @Test
    @DisplayName("実際のWebサイトからHTMLコンテンツを取得 - 成功")
    fun getHtmlContent_RealWebsite_Success() = runTest {
      // Given
      val testUrl = "https://httpbin.org/html" // テスト用のHTMLを返すサイト
      // When
      val result = repository.getHtmlContent(testUrl)
      // Then
      logLong("=== WebView 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("WebViewAndroidTest", "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, "コンテンツが空でない")
    }

    @Test
    @DisplayName("JavaScriptが有効なページからHTMLコンテンツを取得 - 成功")
    fun getHtmlContent_JavaScriptEnabled_Success() = runTest {
      // Given
      val jsUrl = "data:text/html,<html><head><title>JS Test</title></head><body><h1 id='test'>Original</h1><script>document.getElementById('test').innerHTML='Modified by JS';</script></body></html>"
      // When
      val result = repository.getHtmlContent(jsUrl)
      // Then
      logLong("=== JavaScript Test Result ===")
      logLong("Success: ${result.isSuccess}")
      logLong("Content: ${result.getOrNull()}")
      logLong("==============================")

      assertTrue(result.isSuccess, "結果は成功であるべき")
      assertNotNull(result.getOrNull(), "HTMLコンテンツはnullでない")
      // JavaScriptが実行されているかチェック
      assertTrue(
        result.getOrNull()?.contains("Modified by JS") == true,
        "JavaScriptが実行されている"
      )
    }
  }

  @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("=== 404 Error Test Result ===")
      logLong("Success: ${result.isSuccess}")
      logLong("Failure: ${result.isFailure}")
      logLong("Exception: ${result.exceptionOrNull()?.message}")
      logLong("=============================")

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

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

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

    @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("=== WebView 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でもWebViewは何らかのHTMLを返す可能性がある
      // 成功または失敗のいずれかが適切に処理されることを確認
      assertNotNull(result.getOrNull() ?: result.exceptionOrNull(), "結果は適切に処理されるべき")
    }

    @Test
    @DisplayName("無効なURL - エラーハンドリング")
    fun getHtmlContent_InvalidUrl() = runTest {
      // Given
      val testUrl = "invalid-url-format"
      // When
      val result = repository.getHtmlContent(testUrl)
      // Then
      logLong("=== WebView Invalid 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でもWebViewは何らかのHTMLを返す可能性がある
      // 成功または失敗のいずれかが適切に処理されることを確認
      assertNotNull(result.getOrNull() ?: 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("=== WebView Redirect Test ===")
      if (result.isSuccess) {
        val content = result.getOrNull() ?: "(no content)"
        logLong("Redirect handling successful")
        logLong("Content length: ${content.length}")
      } else {
        Log.e("WebViewAndroidTest", "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行目~198行目
テストを実行する処理を定義しています。
61行目~64行目
テストが成功した条件を定義しています。
200行目~213行目
結果のHTMLが4000文字を超える場合、ログ出力時に途中で切捨てられないための関数を定義しています。