0

I am working on an Android project in Android Studio (Electric Eel, 2022.1.1) and I am facing an issue where the UI update task that runs on the MainCoroutineDispatcher never gets executed. My activity makes a call to callBackend during onCreate, which makes a HTTP request to an external API using a coroutine. The response from that request is received in a callback that then sets the value of a TextView on the main thread.

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var quoteContent: TextView
    private val backend: Backend by inject()
    private val idlingResource: CountingIdlingResource? = try {
        get<CountingIdlingResource>()
    } catch (e: Exception) { null }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        quoteContent = binding.quoteContent

        callBackend()
    }

    private fun callBackend(): Job {
        idlingResource?.increment()
        return lifecycleScope.launch(Dispatchers.IO) {
            try {
                backend.quote { quote ->
                    val text = quote?.quote ?: ""
                    idlingResource?.increment()
                    try {
                        if (isActive) {
                            withContext(Dispatchers.Main) {
                                quoteContent.text = text
                            }
                        }
                    } finally {
                        idlingResource?.decrement()
                    }
                }
            } finally {
                idlingResource?.decrement()
            }
        }
    }
}

This implementation of backend.quote looks like this:

    suspend fun quote(callback: suspend (Quote) -> Unit) {
        Log.d(BackendApi::class.java.simpleName, "GET ${baseUrl}inspiration")
        val quote = httpClient.get("${baseUrl}inspiration")
        Log.d(BackendApi::class.java.simpleName, "GOT $quote")
        callback(quote.body<Quote>())
    }

This works as expected when running the app on a virtual device (emulator).

The problem occurs when I try to run the instrumented test, which is using mockwebserver3.MockWebServer.

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @get:Rule
    val idlingResourceRule = IdlingResourceRule("ExampleInstrumentedTest")

    private val testModule: Module = module {
        single { idlingResourceRule.idlingResource }
        single<Backend> {
            val baseUrl: String = getProperty("baseURL")
            Log.i(ExampleInstrumentedTest::class.java.simpleName, "baseURL: ${baseUrl}inspiration")
            Backend(baseUrl)
        }
    }

    @JvmField @Rule
    val serverRule = MockWebServerRule()

    @get:Rule
    val koinTestRule = KoinTestRule.create {
        androidContext(ApplicationProvider.getApplicationContext())
        modules(testModule)
        properties(mapOf(
            "baseURL" to serverRule.server.url("/").toString(),
            "idlingResource" to idlingResourceRule.idlingResource
        ))
    }

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    @Test
    fun onCreate() {
        with(serverRule.server) {
            enqueue(MockResponse().apply {
                headers = Headers.headersOf(
                    "content-type", "application/json",
                )
                setBody("""{
                    |   "quote": "Doing what you like is freedom. Liking what you do is happiness.",
                    |   "author": "Anonymous"
                    |}""".trimMargin())
            })
        }

        val latch = CountDownLatch(1)
        // Get the activity scenario and wait for it to start
        var activity: MainActivity? = null
        activityRule.scenario.use { scenario ->
            scenario.onActivity {
                idlingResourceRule.idlingResource.registerIdleTransitionCallback {
                    latch.countDown()
                }
                latch.await()
                onView(withId(R.id.quote_content))
                    .check(matches(withText("Doing what you like is freedom. Liking what you do is happiness.")))
            }
        }
    }
}

When I use the debugger to run the tests I can see the GET and GOT logs but the quoteContent.text = text is never executed.

I/TestRunner: run started: 1 tests
I/TestRunner: started: onCreate(au.com.phiware.mockwebserverkoinrepro.ExampleInstrumentedTest)
W/zygote: Verification of void au.com.phiware.mockwebserverkoinrepro.ExampleInstrumentedTest.<init>() took 10.827s
W/zygote: Verification of void au.com.phiware.mockwebserverkoinrepro.ExampleInstrumentedTest$testModule$1.invoke(org.koin.core.module.Module) took 100.116ms
D/IdlingRegistry: Registering idling resources: [androidx.test.espresso.idling.CountingIdlingResource@aa5ed72]
W/Settings: Setting always_finish_activities has moved from android.provider.Settings.System to android.provider.Settings.Global, returning read-only value.
D/AppCompatDelegate: Checking for metadata for AppLocalesMetadataHolderService : Service not found
D/LifecycleMonitor: Lifecycle status change: au.com.phiware.mockwebserverkoinrepro.MainActivity@38a7938 in: PRE_ON_CREATE
V/ActivityScenario: Activity lifecycle changed event received but ignored because the reported transition was not ON_CREATE while the last known transition was PRE_ON_CREATE
I/ExampleInstrumentedTest: baseURL: http://localhost:34309/inspiration
D/LifecycleMonitor: Lifecycle status change: au.com.phiware.mockwebserverkoinrepro.MainActivity@38a7938 in: CREATED
V/ActivityScenario: Update currentActivityStage to CREATED, currentActivity=au.com.phiware.mockwebserverkoinrepro.MainActivity@38a7938
D/LifecycleMonitor: Lifecycle status change: au.com.phiware.mockwebserverkoinrepro.MainActivity@38a7938 in: STARTED
V/ActivityScenario: Update currentActivityStage to STARTED, currentActivity=au.com.phiware.mockwebserverkoinrepro.MainActivity@38a7938
D/LifecycleMonitor: Lifecycle status change: au.com.phiware.mockwebserverkoinrepro.MainActivity@38a7938 in: RESUMED
V/ActivityScenario: Update currentActivityStage to RESUMED, currentActivity=au.com.phiware.mockwebserverkoinrepro.MainActivity@38a7938
D/OpenGLRenderer: HWUI GL Pipeline
D/: HostConnection::get() New Host Connection established 0x86c03180, tid 17343
I/OpenGLRenderer: Initialized EGL, version 1.4
D/OpenGLRenderer: Swap behavior 1
W/OpenGLRenderer: Failed to choose config with EGL_SWAP_BEHAVIOR_PRESERVED, retrying without...
D/OpenGLRenderer: Swap behavior 0
D/EGL_emulation: eglCreateContext: 0x9a8048a0: maj 3 min 1 rcv 4
D/EGL_emulation: eglMakeCurrent: 0x9a8048a0: ver 3 1 (tinfo 0x9a803490)
E/eglCodecCommon: glUtilsParamSize: unknow param 0x000082da
E/eglCodecCommon: glUtilsParamSize: unknow param 0x000082da
D/EGL_emulation: eglMakeCurrent: 0x9a8048a0: ver 3 1 (tinfo 0x9a803490)
W/System.err: SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
W/System.err: SLF4J: Defaulting to no-operation (NOP) logger implementation
W/System.err: SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
D/BackendApi: GET http://localhost:34309/inspiration
I/zygote: Background concurrent copying GC freed 7662(887KB) AllocSpace objects, 1(20KB) LOS objects, 57% free, 1115KB/2MB, paused 9.178ms total 65.986ms
D/NetworkSecurityConfig: No Network Security Config specified, using platform default
W/zygote: Verification of void mockwebserver3.MockWebServer$serveConnection$1.invoke() took 549.401ms
D/BackendApi: GOT HttpResponse[http://localhost:34309/inspiration, 200 OK]
I/zygote: Do partial code cache collection, code=15KB, data=30KB
I/zygote: After code cache collection, code=15KB, data=30KB
I/zygote: Increasing code cache capacity to 128KB
I/zygote: Do partial code cache collection, code=61KB, data=46KB
I/zygote: After code cache collection, code=61KB, data=46KB
I/zygote: Increasing code cache capacity to 256KB

And here is the contents of my app's build.gradle for reference:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id "org.jetbrains.kotlin.plugin.serialization" version "1.8.20-RC"
}

android {
    namespace 'au.com.phiware.mockwebserverkoinrepro'
    compileSdk 33

    defaultConfig {
        applicationId "au.com.phiware.mockwebserverkoinrepro"
        minSdk 24
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "au.com.phiware.mockwebserverkoinrepro.InstrumentationTestRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        viewBinding true
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
    implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
    implementation 'io.ktor:ktor-client-core-jvm:2.2.4'
    implementation 'io.ktor:ktor-client-content-negotiation:2.2.4'
    implementation 'io.ktor:ktor-server-content-negotiation:2.2.4'
    implementation 'io.ktor:ktor-serialization-kotlinx-json:2.2.4'
    implementation 'io.ktor:ktor-client-content-negotiation:2.2.4'
    implementation 'io.insert-koin:koin-core-jvm:3.3.3'
    implementation 'io.insert-koin:koin-android:3.3.3'
    implementation 'io.ktor:ktor-client-android:2.2.4'
    implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
    implementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
    androidTestImplementation 'io.insert-koin:koin-test-junit4:3.3.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.11'
    androidTestImplementation 'com.squareup.okhttp3:mockwebserver3-junit4:5.0.0-alpha.11'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
}

The app works as expected outside of the test and I expect that the test would be passing. Running the test without the debugger results in a timeout, which I'm at a loss to explain. I have cleaned and rebuilt the project, and restarted Android Studio, but the issue persists. I have tried using runBlocking and runTest, but the issue persists.

Any suggestions or insights would be greatly appreciated. Thank you!

Corin
  • 2,417
  • 26
  • 23

0 Answers0