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

