okhttp icon indicating copy to clipboard operation
okhttp copied to clipboard

Silently throwing robolectric RunTimeException in unit test

Open vincent-paing opened this issue 2 months ago • 4 comments

OKHttpVersion used : 5.2.0

Reproduction Steps

  • Create an empty Android project
  • Add OkHttp depdencies
// define a BOM and its version
implementation(platform("com.squareup.okhttp3:okhttp-bom:5.2.0"))

// define any required OkHttp artifacts without version
implementation("com.squareup.okhttp3:okhttp")
implementation("com.squareup.okhttp3:logging-interceptor")
  • In ExampleUnitTest, add this OkHttpClient.Builder().build() in the first line of addition_isCorrect.
class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        OkHttpClient.Builder().build()
        assertEquals(4, 2 + 2)
    }
}
  • Run the test
  • The test passes but there's runtime exception being thrown silently that's causing a a lot of noise in the test output
Possibly running android unit test without robolectric
java.lang.RuntimeException: Method isLoggable in android.util.Log not mocked. See https://developer.android.com/r/studio-ui/build/not-mocked for details.
	at android.util.Log.isLoggable(Log.java)
	at okhttp3.internal.platform.android.AndroidLog.enableLogging(AndroidLog.kt:127)
	at okhttp3.internal.platform.android.AndroidLog.enable(AndroidLog.kt:109)
	at okhttp3.internal.platform.PlatformRegistry.findPlatform(PlatformRegistry.kt:25)
	at okhttp3.internal.platform.Platform$Companion.findPlatform(Platform.kt:225)
	at okhttp3.internal.platform.Platform$Companion.access$findPlatform(Platform.kt:203)
	at okhttp3.internal.platform.Platform.<clinit>(Platform.kt:204)
	at okhttp3.OkHttpClient.<init>(OkHttpClient.kt:300)
	at okhttp3.OkHttpClient$Builder.build(OkHttpClient.kt:1384)
	at com.example.okhttptest.ExampleUnitTest.addition_isCorrect(ExampleUnitTest.kt:17)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:112)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:40)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:54)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:53)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:92)
	at jdk.proxy1/jdk.proxy1.$Proxy4.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:183)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:132)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:121)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

vincent-paing avatar Oct 09 '25 03:10 vincent-paing

As of version 5.0 OkHttp has an Android variant and will access Android APIs without guards. As such, you need to provide the appropriate test environment, which is either: enabling return default values true, Robolectric, or running unit tests via instrumentation on emulators or real devices.

JakeWharton avatar Oct 09 '25 03:10 JakeWharton

As of version 5.0 OkHttp has an Android variant and will access Android APIs without guards. As such, you need to provide the appropriate test environment, which is either: enabling return default values true, Robolectric, or running unit tests via instrumentation on emulators or real devices.

I found a related issue that the test start crashing with paparazzi plugin added. To replicate, add id("app.cash.paparazzi") version "2.0.0-alpha02" at the module level gradle , and this silent error become fatal crash.

boolean android.util.Log.isLoggable(java.lang.String, int)'
java.lang.UnsatisfiedLinkError: 'boolean android.util.Log.isLoggable(java.lang.String, int)'
	at android.util.Log.isLoggable(Native Method)
	at okhttp3.internal.platform.android.AndroidLog.enableLogging(AndroidLog.kt:127)
	at okhttp3.internal.platform.android.AndroidLog.enable(AndroidLog.kt:109)
	at okhttp3.internal.platform.PlatformRegistry.findPlatform(PlatformRegistry.kt:25)
	at okhttp3.internal.platform.Platform$Companion.findPlatform(Platform.kt:225)
	at okhttp3.internal.platform.Platform$Companion.access$findPlatform(Platform.kt:203)
	at okhttp3.internal.platform.Platform.<clinit>(Platform.kt:204)
	at okhttp3.OkHttpClient.<init>(OkHttpClient.kt:300)

I've tired returning default value true in gradle with following code but that didn't fix it.

testOptions {
    unitTests.all {
        unitTests.isReturnDefaultValues = true
    }
}

Should this paparazzi plugin integration also be an issue or an expected behavior? If it'll be considered as issue, I can up a reproducible repository and open an issue in Paparazzi.

A workaround that works for us now is we can explicitly override to use jvm artifact for test by adding testImplementation "com.squareup.okhttp3:okhttp-jvm:5.2.0" since we don't use Robolectric for our unit tests.

vincent-paing avatar Oct 09 '25 03:10 vincent-paing

Paparazzi brings a JVM version of the compiled Android framework which is trying to link the native code that Log bottoms out in. It sits in the classpath before the mockable jar that AGP creates, which is why that setting no longer has any effect.

The behavior you're seeing is the same you'd see for any Android library that touched Log first before any other Android APIs.

Forcing the JVM variant may work for now, although it's not guaranteed that Android-specific APIs may come in the future which would not be available on the JVM variant causing NoClassDefFoundError or NoSuchMethodException. Aside from the three options above, you could also swap out OkHttp with something else either at a higher-layer of abstraction (like a service interface) or even just choosing to have your entry point take a Call.Factory and making a simple fake.

JakeWharton avatar Oct 09 '25 03:10 JakeWharton

This seems like something we should make safe https://github.com/square/okhttp/pull/9137

At least the intent was there.

yschimke avatar Oct 11 '25 06:10 yschimke