Skip to content

Instantly share code, notes, and snippets.

@ephemient
Last active June 21, 2022 02:53
Show Gist options
  • Save ephemient/01d6e5766e6f8ea02839b4d7c3f94e55 to your computer and use it in GitHub Desktop.
Save ephemient/01d6e5766e6f8ea02839b4d7c3f94e55 to your computer and use it in GitHub Desktop.
import java.lang.reflect.Parameter
import java.util.stream.Stream
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.callSuspendBy
import kotlin.reflect.full.functions
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.isSupertypeOf
import kotlin.reflect.typeOf
import kotlin.streams.asStream
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestTemplate
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.Extension
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import org.junit.jupiter.api.extension.TestTemplateInvocationContext
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider
interface CoroutineTest {
val testScope: TestScope
get() = TestScope()
val dispatchTimeoutMs: Long
get() = 60_000L
@TestTemplate
@ExtendWith(SuspendTestContextProvider::class)
fun runSuspendTest(testCase: suspend TestScope.() -> Unit) = testScope.runTest(dispatchTimeoutMs, testCase)
}
private fun Parameter.isSuspendUnitFun(): Boolean =
parameterizedType.typeName == "kotlin.jvm.functions.Function2<? super kotlinx.coroutines.test.TestScope, ? super kotlin.coroutines.Continuation<? super kotlin.Unit>, ?>"
private class SuspendTestContextProvider : TestTemplateInvocationContextProvider {
override fun supportsTestTemplate(context: ExtensionContext): Boolean =
context.requiredTestMethod.parameters.any { it.isSuspendUnitFun() }
override fun provideTestTemplateInvocationContexts(context: ExtensionContext): Stream<TestTemplateInvocationContext> =
context.requiredTestClass.kotlin.functions.asSequence().mapNotNull {
if (it.isSuspend && it.hasAnnotation<Test>()) SuspendTestContext(it) else null
}.asStream()
}
private class SuspendTestContext(private val member: KFunction<*>) : TestTemplateInvocationContext {
override fun getDisplayName(invocationIndex: Int): String = member.name
override fun getAdditionalExtensions(): List<Extension> = listOf(SuspendTestParameterProvider(member))
}
private class SuspendTestParameterProvider(private val member: KFunction<*>) : ParameterResolver {
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean =
parameterContext.parameter.isSuspendUnitFun()
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): suspend TestScope.() -> Unit = testScope@{
member.callSuspendBy(
buildMap {
for (parameter in member.parameters) {
when {
parameter.kind == KParameter.Kind.INSTANCE -> put(parameter, parameterContext.target.get())
with(parameter.type) {
isSubtypeOf(typeOf<CoroutineScope>()) && isSupertypeOf(typeOf<TestScope>())
} -> put(parameter, this@testScope)
parameter.isOptional -> {}
// Unable to use other parameter resolvers, https://github.com/junit-team/junit5/issues/378
else -> TODO("Unsupported parameter: $parameter")
}
}
}
)
}
}
import kotlinx.coroutines.test.TestScope
import org.junit.jupiter.api.Test
class ExampleTest : CoroutineTest {
@Test
fun blockingTest() {
println("ok")
}
@Test
suspend fun suspendTest() {
println("ok")
}
@Test
suspend fun TestScope.suspendTest2() {
println(testScheduler.currentTime)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment