Skip to content

Commit

Permalink
feat(robolectric-extension): Add robolectric extension (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
warnyul authored Mar 14, 2024
1 parent b9f83db commit adfa346
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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<Void>,
invocationContext: ReflectiveInvocationContext<Method>,
extensionContext: ExtensionContext,
) {
logger.trace { "interceptBeforeAllMethod ${extensionContext.requiredTestClass}" }
invocation.skip()
}

override fun interceptBeforeEachMethod(
invocation: InvocationInterceptor.Invocation<Void>,
invocationContext: ReflectiveInvocationContext<Method>,
extensionContext: ExtensionContext,
) {
logger.trace { "interceptBeforeEachMethod ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" }
robolectricTestRunnerHelper.runOnMainThreadWithRobolectric {
super.interceptBeforeEachMethod(invocation, invocationContext, extensionContext)
}
}

override fun interceptDynamicTest(
invocation: InvocationInterceptor.Invocation<Void>,
invocationContext: DynamicTestInvocationContext,
extensionContext: ExtensionContext,
) {
logger.trace { "interceptDynamicTest ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" }
robolectricTestRunnerHelper.runOnMainThreadWithRobolectric {
super.interceptDynamicTest(invocation, invocationContext, extensionContext)
}
}

override fun interceptTestMethod(
invocation: InvocationInterceptor.Invocation<Void>,
invocationContext: ReflectiveInvocationContext<Method>,
extensionContext: ExtensionContext,
) {
logger.trace { "interceptTestMethod ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" }
robolectricTestRunnerHelper.runOnMainThreadWithRobolectric {
super.interceptTestMethod(invocation, invocationContext, extensionContext)
}
}

override fun interceptTestTemplateMethod(
invocation: InvocationInterceptor.Invocation<Void>,
invocationContext: ReflectiveInvocationContext<Method>,
extensionContext: ExtensionContext,
) {
logger.trace { "interceptTestTemplateMethod ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" }
robolectricTestRunnerHelper.runOnMainThreadWithRobolectric {
super.interceptTestTemplateMethod(invocation, invocationContext, extensionContext)
}
}

override fun interceptAfterEachMethod(
invocation: InvocationInterceptor.Invocation<Void>,
invocationContext: ReflectiveInvocationContext<Method>,
extensionContext: ExtensionContext,
) {
logger.trace { "interceptAfterEachMethod ${extensionContext.requiredTestClass}::${extensionContext.requiredTestMethod}" }
robolectricTestRunnerHelper.runOnMainThreadWithRobolectric {
super.interceptAfterEachMethod(invocation, invocationContext, extensionContext)
}
}

override fun interceptAfterAllMethod(
invocation: InvocationInterceptor.Invocation<Void>,
invocationContext: ReflectiveInvocationContext<Method>,
extensionContext: ExtensionContext,
) {
logger.trace { "interceptAfterAllMethod ${extensionContext.requiredTestClass}" }
robolectricTestRunnerHelper.runOnMainThreadWithRobolectric {
super.interceptAfterAllMethod(invocation, invocationContext, extensionContext)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Class<*>>(SdkSandboxClassLoader::class.java, classLoader.javaClass)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Context>()
assertIs<MyTestApplication>(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()
}
}
}

0 comments on commit adfa346

Please sign in to comment.