Skip to content

Commit

Permalink
Fix not working test if coroutineTestScope = true (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
uOOOO authored and LeoColman committed Aug 15, 2024
1 parent 59bb088 commit 06a78c5
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ dependencies {
implementation("androidx.test:core-ktx:1.5.0")
testImplementation(project(":kotest-extensions-android"))
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
testImplementation("org.robolectric:robolectric:4.12.2")
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test:core:1.5.0")
androidTestImplementation("androidx.test:rules:1.5.0")

}

tasks.withType<Test> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,25 @@ package br.com.colman.kotest.android.extensions

import android.app.Application
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.test.core.app.ApplicationProvider
import br.com.colman.kotest.TestApplication
import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest
import io.kotest.core.coroutines.backgroundScope
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.core.spec.style.StringSpec
import io.kotest.core.test.testCoroutineScheduler
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.equals.shouldBeEqual
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.robolectric.Shadows.shadowOf

private class MockApplication : Application()

Expand Down Expand Up @@ -67,8 +79,9 @@ abstract class ContainedRobolectricRunnerMergeOverwriteTest : StringSpec({
@RobolectricTest(sdk = Build.VERSION_CODES.O_MR1)
class ContainedRobolectricRunnerMergeOverwriteO_MR1Test : ContainedRobolectricRunnerMergeOverwriteTest()

@OptIn(ExperimentalStdlibApi::class, ExperimentalCoroutinesApi::class)
@RobolectricTest
class ContainedRobolectricRunnerDefaultApplicationBehaviorSpecTest : BehaviorSpec({
class ContainedRobolectricRunnerBehaviorSpecTest : BehaviorSpec({
Context("Get the Application defined in AndroidManifest.xml") {
Given("A application context") {
val applicationContext = ApplicationProvider.getApplicationContext<Application>()
Expand All @@ -82,4 +95,48 @@ class ContainedRobolectricRunnerDefaultApplicationBehaviorSpecTest : BehaviorSpe
}
}
}

coroutineTestScope = true

Context("Collect the flow in TestScope") {
Given("Some numbers and a SharedFlow") {
val numbers = listOf(1, 2, 3)
val sharedFlow = MutableSharedFlow<Int>()

And("Launch to collect the flow") {
val collectedNumbers = mutableListOf<Int>()
backgroundScope.launch(UnconfinedTestDispatcher(testCoroutineScheduler)) {
sharedFlow.collect { collectedNumbers.add(it) }
}

When("Emit the numbers") {
numbers.forEach { launch { sharedFlow.emit(it) } }

testCoroutineScheduler.advanceUntilIdle()

Then("Collected the same numbers") {
collectedNumbers shouldBeEqual numbers
}
}
}
}
}

Context("Unit testing Android Handler using Robolectric") {
Given("An Android Handler") {
val handler = Handler(Looper.getMainLooper())

When("Add a number on the Handler") {
val numbers = mutableListOf<Long>()
val currentTime = System.currentTimeMillis()
handler.post { numbers.add(currentTime) }

shadowOf(Looper.getMainLooper()).idle()

Then("The Number added the list") {
numbers.shouldNotBeEmpty().shouldContain(currentTime)
}
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal class ContainedRobolectricRunner(
override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration {
return InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method))
.doNotAcquirePackage("io.kotest")
.doNotAcquirePackage("kotlinx.coroutines")
.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ import io.kotest.core.spec.Spec
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult
import io.kotest.core.test.isRootTest
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import org.robolectric.annotation.Config
import org.robolectric.internal.bytecode.Sandbox
import java.util.WeakHashMap
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.time.Duration
import java.util.WeakHashMap
import java.util.concurrent.ExecutorService

/**
* We override TestCaseExtension to configure the Robolectric environment because TestCase intercept
Expand Down Expand Up @@ -113,21 +109,14 @@ class RobolectricExtension : ConstructorExtension, TestCaseExtension {
execute: suspend (TestCase) -> TestResult,
): TestResult {
val containedRobolectricRunner = runnerMap[testCase.spec]!!
// Using sdkEnvironment.executorService to ensure Robolectric's
// looper state doesn't carry over to the next test class.
val executorService =
Sandbox::class.java
.getValue<ExecutorService>(containedRobolectricRunner.sdkEnvironment)
return withContext(executorService.asCoroutineDispatcher()) {
if (testCase.isRootTest()) {
containedRobolectricRunner.containedBefore()
}
val result = execute(testCase)
if (testCase.isRootTest()) {
containedRobolectricRunner.containedAfter()
}
result
if (testCase.isRootTest()) {
containedRobolectricRunner.containedBefore()
}
val result = execute(testCase)
if (testCase.isRootTest()) {
containedRobolectricRunner.containedAfter()
}
return result
}
}

Expand Down

0 comments on commit 06a78c5

Please sign in to comment.