You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android网络层JUnit测试遇Robolectric间歇性异常:排查与解决

问题:Android网络层JUnit测试的间歇性日志问题

我在为Android应用的网络层编写JUnit测试时,日志中间歇性出现两类问题:

1. SQLiteConnection未关闭警告

System.logW: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks.
java.lang.Throwable: Explicit termination method 'SQLiteConnection.close' not called
    at dalvik.system.CloseGuard.$$robo$$dalvik_system_CloseGuard$openWithCallSite(CloseGuard.java:288)
    at dalvik.system.CloseGuard.openWithCallSite(CloseGuard.java)
    at dalvik.system.CloseGuard.$$robo$$dalvik_system_CloseGuard$open(CloseGuard.java:257)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.base/java.lang.reflect.Method.invoke(Unknown Source)
    at org.robolectric.shadows.ShadowCloseGuard$CloseGuardReflector$$Reflector289.open(Unknown Source)
    at org.robolectric.shadows.ShadowCloseGuard.open(ShadowCloseGuard.java:38)
    at dalvik.system.CloseGuard.open(CloseGuard.java)
    at android.database.sqlite.SQLiteConnection.$$robo$$android_database_sqlite_SQLiteConnection$__constructor__(SQLiteConnection.java:182)
    at android.database.sqlite.SQLiteConnection.<init>(SQLiteConnection.java)
    at android.database.sqlite.SQLiteConnection.$$robo$$android_database_sqlite_SQLiteConnection$open(SQLiteConnection.java:202)
    at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java)
    at android.database.sqlite.SQLiteConnectionPool.$$robo$$android_database_sqlite_SQLiteConnectionPool$openConnectionLocked(SQLiteConnectionPool.java:512)
    at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java)
    at android.database.sqlite.SQLiteConnectionPool.$$robo$$android_database_sqlite_SQLiteConnectionPool$open(SQLiteConnectionPool.java:210)
    at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java)
    at android.database.sqlite.SQLiteConnectionPool.$$robo$$android_database_sqlite_SQLiteConnectionPool$open(SQLiteConnectionPool.java:202)
    at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java)
    at android.database.sqlite.SQLiteDatabase.$$robo$$android_database_sqlite_SQLiteDatabase$openInner(SQLiteDatabase.java:1085)
    at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java)
    at android.database.sqlite.SQLiteDatabase.$$robo$$android_database_sqlite_SQLiteDatabase$open(SQLiteDatabase.java:1065)
    at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java)
    at android.database.sqlite.SQLiteDatabase.$$robo$$android_database_sqlite_SQLiteDatabase$openDatabase(SQLiteDatabase.java:929)
    at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java)
    at android.database.sqlite.SQLiteDatabase.$$robo$$android_database_sqlite_SQLiteDatabase$openDatabase(SQLiteDatabase.java:918)
    at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java)
    at android.database.sqlite.SQLiteOpenHelper.$$robo$$android_database_sqlite_SQLiteOpenHelper$getDatabaseLocked(SQLiteOpenHelper.java:373)
    at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java)
    at android.database.sqlite.SQLiteOpenHelper.$$robo$$android_database_sqlite_SQLiteOpenHelper$getWritableDatabase(SQLiteOpenHelper.java:316)
    at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java)
    at com.google.android.datatransport.runtime.scheduling.persistence.SQLiteEventStore.retryIfDbLocked(SQLiteEventStore.java:582)
    at com.google.android.datatransport.runtime.scheduling.persistence.SQLiteEventStore.getDb(SQLiteEventStore.java:95)
    at com.google.android.datatransport.runtime.scheduling.persistence.SQLiteEventStore.runCriticalSection(SQLiteEventStore.java:765)
    at com.google.android.datatransport.runtime.scheduling.jobscheduling.WorkInitializer.lambda$ensureContextsScheduled$1(WorkInitializer.java:54)
    at com.google.android.datatransport.runtime.SafeLoggingExecutor$SafeLoggingRunnable.run(SafeLoggingExecutor.java:47)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.base/java.lang.Thread.run(Unknown Source)

2. 临时目录销毁失败错误

ERROR: Failed to destroy temp directory
java.nio.file.DirectoryNotEmptyException: /var/folders/pr/pzzm0m_17273s8dcc2fw7b6w0000gn/T/robolectric-nativeruntime16490046703417862469/fonts
    at java.base/sun.nio.fs.UnixFileSystemProvider.implDelete(Unknown Source)
    at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(Unknown Source)
    at java.base/java.nio.file.Files.delete(Unknown Source)
    at org.robolectric.util.TempDirectory$1.postVisitDirectory(TempDirectory.java:129)
    at org.robolectric.util.TempDirectory$1.postVisitDirectory(TempDirectory.java:119)
    at java.base/java.nio.file.Files.walkFileTree(Unknown Source)
    at java.base/java.nio.file.Files.walkFileTree(Unknown Source)
    at org.robolectric.util.TempDirectory.clearDirectory(TempDirectory.java:119)
    at org.robolectric.util.TempDirectory.destroy(TempDirectory.java:111)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.base/java.lang.Thread.run(Unknown Source)

我的JUnit测试代码

import androidx.test.core.app.ApplicationProvider
import com.google.firebase.FirebaseApp
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.observer.ResponseObserver
import io.ktor.client.request.header
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.middle.earth.lotr.BuildConfig
import org.middle.earth.lotr.data.remote.dto.character.CharacterResponse
import org.middle.earth.lotr.di.NETWORK_TIME_OUT
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(minSdk = 26, maxSdk = 34)
class TheOneApiNetworkTest {

    @Before
    fun setUp() {
        FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext())
    }

    @Test
    fun character() {
        val httpClient = getHttpClient(mockEngineCharactersOk)

        runBlocking {
            val response = TheOneApiHttpService(httpClient).character(1)
            val characters = response.body<CharacterResponse>()
            assertEquals(characters.characters.isNotEmpty(), true)
        }

    }

    @Test(expected = ClientRequestException::class)
    fun characterBadRequest() {
        val httpClient = getHttpClient(mockEngineBadRequest)

        runBlocking {
            TheOneApiHttpService(httpClient).character(1)
        }
    }

    private fun getHttpClient(mockEngine: MockEngine) = HttpClient(mockEngine) {

        expectSuccess = true

        install(ContentNegotiation) {
            json(
                Json {
                    prettyPrint = true
                    isLenient = true
                    useAlternativeNames = true
                    ignoreUnknownKeys = false
                    encodeDefaults = true
                }
            )
        }

        install(HttpTimeout) {
            requestTimeoutMillis = NETWORK_TIME_OUT
            connectTimeoutMillis = NETWORK_TIME_OUT
            socketTimeoutMillis = NETWORK_TIME_OUT
        }

        install(Logging) {
            logger = object : Logger {
                override fun log(message: String) {
                    println("log() called with: message = $message")
                }
            }
            level = LogLevel.ALL
        }

        install(ResponseObserver) {
            onResponse { response ->
                println("providesHttpClient() called with: response = $response
${response.status.value}")
            }
        }

        defaultRequest {
            header(HttpHeaders.ContentType, ContentType.Application.Json)
            header(HttpHeaders.Authorization, "Bearer ${BuildConfig.THE_ONE_API_ACCESS_TOKEN}")
            header(HttpHeaders.UserAgent, "Android Mock User Agent")
        }
    }
}

测试相关Gradle依赖

testImplementation("junit:junit:4.13.2")
testImplementation("org.slf4j:slf4j-api:2.0.11")
testImplementation("org.slf4j:slf4j-simple:2.0.11")
testImplementation("androidx.test:core:1.5.0")
testImplementation("io.ktor:ktor-client-mock:2.3.7")
testImplementation("io.mockk:mockk:1.13.9")
testImplementation("org.robolectric:robolectric:4.11.1")

请问我哪里操作有误?这些信息是否需要关注?有没有办法消除或解决这些问题?


问题分析与解决

1. SQLiteConnection未关闭警告

  • 问题根源:从堆栈跟踪可以看出,这个警告来自Google Data Transport库(com.google.android.datatransport),是Firebase初始化后触发的后台任务使用SQLite但未正确关闭连接导致的,和你的网络测试代码无关。Robolectric的CloseGuard检测机制会捕捉到这类资源泄漏并抛出警告。
  • 是否需要关注:测试环境下不会影响测试结果,只是资源泄漏提示;生产环境中Data Transport库会自行管理连接生命周期,不会有问题。如果只是为了干净的测试日志,可以处理。
  • 解决办法:
    • 方案一:禁用Robolectric的CloseGuard检测,在测试类的@Config注解中添加系统属性:
      @Config(minSdk = 26, maxSdk = 34, systemProperty = ["robolectric.closeguard.enabled", "false"])
      
    • 方案二:如果你的网络测试不需要Firebase服务,直接去掉setUp方法中的FirebaseApp.initializeApp调用,因为你用了MockEngine模拟网络请求,不需要真实的Firebase初始化。

2. 临时目录销毁失败错误

  • 问题根源:这是Robolectric 4.11.x版本的已知bug,测试结束清理临时文件时,字体目录无法被彻底删除,属于框架层面的问题,和你的代码无关。
  • 是否需要关注:不影响测试执行和结果,只是测试结束后的清理错误,不会导致测试失败。
  • 解决办法:
    • 方案一:升级Robolectric到4.12及以上版本,该版本已修复此临时目录清理问题。
    • 方案二:如果暂时无法升级,跳过临时目录清理,在Gradle的测试配置中添加JVM参数:
      test {
          jvmArgs "-Drobolectric.dontDeleteTempDir=true"
      }
      
      或者在测试类的@Config注解中添加系统属性:
      @Config(minSdk = 26, maxSdk = 34, systemProperty = ["robolectric.dontDeleteTempDir", "true"])
      
      注意:此方案会在系统临时目录留下Robolectric的临时文件,需要定期手动清理。

内容的提问来源于stack exchange,提问作者Hector

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 16:52:34