From adfa346d7dba1ca627d9aa7c00c2fa25aec31e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Varga?= Date: Thu, 14 Mar 2024 08:35:55 +0100 Subject: [PATCH] feat(robolectric-extension): Add robolectric extension (#13) --- .../robolectric/RobolectricExtension.kt | 152 +++++++++++++++++- .../RobolectricExtensionClassLoaderTest.kt | 48 ++++++ .../RobolectricExtensionSelfTest.kt | 66 ++++++++ 3 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionClassLoaderTest.kt create mode 100644 robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt diff --git a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtension.kt b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtension.kt index d689a01..aa0b0a2 100644 --- a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtension.kt +++ b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtension.kt @@ -1,5 +1,153 @@ package tech.apter.junit.jupiter.robolectric -import org.junit.jupiter.api.extension.Extension +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.concurrent.atomic.AtomicBoolean +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.DynamicTestInvocationContext +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.InvocationInterceptor +import org.junit.jupiter.api.extension.ReflectiveInvocationContext +import org.junit.platform.commons.util.ReflectionUtils +import tech.apter.junit.jupiter.robolectric.internal.JUnit5RobolectricTestRunnerHelper +import tech.apter.junit.jupiter.robolectric.internal.createLogger +import tech.apter.junit.jupiter.robolectric.internal.runOnMainThread +import tech.apter.junit.jupiter.robolectric.internal.runOnMainThreadWithRobolectric +import tech.apter.junit.jupiter.robolectric.internal.runWithRobolectric -class RobolectricExtension : Extension +class RobolectricExtension : + InvocationInterceptor, + BeforeAllCallback, + BeforeEachCallback, + AfterEachCallback, + AfterAllCallback { + private inline val logger get() = createLogger() + private val beforeAllFired = AtomicBoolean(false) + private val robolectricTestRunnerHelper by lazy { JUnit5RobolectricTestRunnerHelper() } + + override fun beforeAll(context: ExtensionContext) { + logger.trace { "beforeAll ${context.requiredTestClass.name}" } + robolectricTestRunnerHelper.createTestEnvironmentForClass(context.requiredTestClass) + } + + override fun beforeEach(context: ExtensionContext) { + logger.trace { "beforeEach ${context.requiredTestClass.name}::${context.requiredTestMethod.name}" } + robolectricTestRunnerHelper.createTestEnvironmentForMethod(context.requiredTestMethod) + robolectricTestRunnerHelper.runOnMainThreadWithRobolectric { + if (!beforeAllFired.getAndSet(true)) { + invokeBeforeAllMethods(testClass = context.requiredTestClass) + } + beforeEach(context.requiredTestMethod) + } + } + + private fun invokeBeforeAllMethods(testClass: Class<*>) { + val beforeAllMethods = testClass + .methods + .filter { + it.getAnnotation(BeforeAll::class.java) != null && + Modifier.isStatic(it.modifiers) + } + + beforeAllMethods.forEach { + logger.trace { "invoke beforeAll ${it.name}" } + + ReflectionUtils.invokeMethod(it, null) + } + } + + override fun afterEach(context: ExtensionContext) { + logger.trace { "afterEach ${context.requiredTestClass.name}::${context.requiredTestMethod.name}" } + robolectricTestRunnerHelper.runOnMainThread { + runWithRobolectric { + afterEach(context.requiredTestMethod) + } + afterEachFinally() + } + } + + override fun afterAll(context: ExtensionContext) { + logger.trace { "afterAll ${context.requiredTestClass.name}" } + robolectricTestRunnerHelper.clearCachedRobolectricTestRunnerEnvironment() + beforeAllFired.set(false) + } + + override fun interceptBeforeAllMethod( + invocation: InvocationInterceptor.Invocation, + invocationContext: ReflectiveInvocationContext, + extensionContext: ExtensionContext, + ) { + logger.trace { "interceptBeforeAllMethod ${extensionContext.requiredTestClass}" } + invocation.skip() + } + + override fun interceptBeforeEachMethod( + invocation: InvocationInterceptor.Invocation, + invocationContext: ReflectiveInvocationContext, + extensionContext: ExtensionContext, + ) { + logger.trace { "interceptBeforeEachMethod ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" } + robolectricTestRunnerHelper.runOnMainThreadWithRobolectric { + super.interceptBeforeEachMethod(invocation, invocationContext, extensionContext) + } + } + + override fun interceptDynamicTest( + invocation: InvocationInterceptor.Invocation, + invocationContext: DynamicTestInvocationContext, + extensionContext: ExtensionContext, + ) { + logger.trace { "interceptDynamicTest ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" } + robolectricTestRunnerHelper.runOnMainThreadWithRobolectric { + super.interceptDynamicTest(invocation, invocationContext, extensionContext) + } + } + + override fun interceptTestMethod( + invocation: InvocationInterceptor.Invocation, + invocationContext: ReflectiveInvocationContext, + extensionContext: ExtensionContext, + ) { + logger.trace { "interceptTestMethod ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" } + robolectricTestRunnerHelper.runOnMainThreadWithRobolectric { + super.interceptTestMethod(invocation, invocationContext, extensionContext) + } + } + + override fun interceptTestTemplateMethod( + invocation: InvocationInterceptor.Invocation, + invocationContext: ReflectiveInvocationContext, + extensionContext: ExtensionContext, + ) { + logger.trace { "interceptTestTemplateMethod ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" } + robolectricTestRunnerHelper.runOnMainThreadWithRobolectric { + super.interceptTestTemplateMethod(invocation, invocationContext, extensionContext) + } + } + + override fun interceptAfterEachMethod( + invocation: InvocationInterceptor.Invocation, + invocationContext: ReflectiveInvocationContext, + extensionContext: ExtensionContext, + ) { + logger.trace { "interceptAfterEachMethod ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" } + robolectricTestRunnerHelper.runOnMainThreadWithRobolectric { + super.interceptAfterEachMethod(invocation, invocationContext, extensionContext) + } + } + + override fun interceptAfterAllMethod( + invocation: InvocationInterceptor.Invocation, + invocationContext: ReflectiveInvocationContext, + extensionContext: ExtensionContext, + ) { + logger.trace { "interceptAfterAllMethod ${extensionContext.requiredTestClass}" } + robolectricTestRunnerHelper.runOnMainThreadWithRobolectric { + super.interceptAfterAllMethod(invocation, invocationContext, extensionContext) + } + } +} diff --git a/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionClassLoaderTest.kt b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionClassLoaderTest.kt new file mode 100644 index 0000000..ec124b7 --- /dev/null +++ b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionClassLoaderTest.kt @@ -0,0 +1,48 @@ +package tech.apter.junit.jupiter.robolectric + +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.extension.ExtendWith +import org.robolectric.internal.AndroidSandbox.SdkSandboxClassLoader +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@ExtendWith(RobolectricExtension::class) +class RobolectricExtensionClassLoaderTest { + + @BeforeTest + fun setUp() { + assertSdkClassLoader() + } + + @AfterTest + fun tearDown() { + assertSdkClassLoader() + } + + @Test + fun testClassLoader() { + assertSdkClassLoader() + } + + companion object { + @BeforeAll + @JvmStatic + fun setUpClass() { + assertSdkClassLoader() + } + + @AfterAll + @JvmStatic + fun tearDownClass() { + assertSdkClassLoader() + } + + private fun assertSdkClassLoader() { + val classLoader = Thread.currentThread().contextClassLoader + assertEquals>(SdkSandboxClassLoader::class.java, classLoader.javaClass) + } + } +} diff --git a/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt new file mode 100644 index 0000000..f0e21c6 --- /dev/null +++ b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt @@ -0,0 +1,66 @@ +package tech.apter.junit.jupiter.robolectric + +import android.app.Application +import android.content.Context +import android.os.Looper +import androidx.test.core.app.ApplicationProvider +import java.util.concurrent.atomic.AtomicInteger +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.extension.ExtendWith +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@ExtendWith(RobolectricExtension::class) +@Config(application = RobolectricExtensionSelfTest.MyTestApplication::class) +class RobolectricExtensionSelfTest { + + @Test + fun shouldInitializeAndBindApplicationButNotCallOnCreate() { + val application = ApplicationProvider.getApplicationContext() + assertIs(application, "application") + assertTrue("onCreateCalled") { application.onCreateWasCalled } + if (RuntimeEnvironment.useLegacyResources()) { + assertNotNull(RuntimeEnvironment.getAppResourceTable(), "Application resource loader") + } + } + + @Test + fun `Before the test before class should be fired one`() { + assertEquals(1, beforeAllFired.get()) + } + + companion object { + private var onTerminateCalledFromMain: Boolean? = null + private val beforeAllFired = AtomicInteger(0) + + @BeforeAll + @JvmStatic + fun setUpClass() { + beforeAllFired.incrementAndGet() + } + + @AfterAll + @JvmStatic + fun tearDown() { + beforeAllFired.set(0) + } + } + + class MyTestApplication : Application() { + internal var onCreateWasCalled = false + + override fun onCreate() { + this.onCreateWasCalled = true + } + + override fun onTerminate() { + onTerminateCalledFromMain = Looper.getMainLooper().thread === Thread.currentThread() + } + } +}