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!