Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow LooperMode annotation on nested tests and test methods #59

Merged
merged 2 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ Robolectric's in-memory environment.

## Current Limitations

* **Parallel Execution:** Parallel test execution only supported with classes.
* **Parallel Execution:** Parallel test execution only experimentally supported with classes.
* **Configuration:**
* Robolectric `@Config`'s sdk parameter annotation can only be set on outermost test class.
* `@ResourcesMode`, `@LooperMode`, `GraphicsMode` annotations can only be set on outermost test
class.
* `@GraphicsMode` annotations can only be set on outermost test class.
* **Experimental Status:** This extension is still under development, and its API might change in
future versions.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ internal class JUnit5RobolectricSandboxManager @Inject constructor(
instrumentationConfiguration = instrumentationConfig,
sdk = sdk,
resourcesMode = resourcesMode,
looperMode = looperMode,
graphicsMode = graphicsMode,
)
// Return the same sandbox for nested tests
Expand All @@ -52,7 +51,6 @@ internal class JUnit5RobolectricSandboxManager @Inject constructor(
private val instrumentationConfiguration: InstrumentationConfiguration,
private val sdk: Sdk,
private val resourcesMode: ResourcesMode,
private val looperMode: LooperMode.Mode,
private val graphicsMode: GraphicsMode.Mode,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,20 @@ internal class JUnit5RobolectricTestRunner(
frameworkMethod: FrameworkMethod,
bootstrappedMethod: Method,
) {
synchronized(beforeTestLock) {
logger.trace { "runBeforeTest ${bootstrappedMethod.declaringClass.simpleName}::${bootstrappedMethod.name}" }
super.beforeTest(sdkEnvironment, frameworkMethod, bootstrappedMethod)
}
logger.trace { "runBeforeTest ${bootstrappedMethod.declaringClass.simpleName}::${bootstrappedMethod.name}" }
super.beforeTest(sdkEnvironment, frameworkMethod, bootstrappedMethod)
}

fun runAfterTest(frameworkMethod: FrameworkMethod, bootstrappedMethod: Method) {
logger.trace { "runAfterTest ${frameworkMethod.declaringClass.simpleName}::${frameworkMethod.name}" }
super.afterTest(frameworkMethod, bootstrappedMethod)
}

fun runFinallyAfterTest(frameworkMethod: FrameworkMethod) {
fun runFinallyAfterTest(sdkEnvironment: Sandbox, frameworkMethod: FrameworkMethod) {
logger.trace { "runFinallyAfterTest ${frameworkMethod.declaringClass.simpleName}::${frameworkMethod.name}" }
super.finallyAfterTest(frameworkMethod)
sdkEnvironment.clearShadowLooperCache()
sdkEnvironment.resetLooper()
}

override fun createClassLoaderConfig(method: FrameworkMethod): InstrumentationConfiguration {
Expand Down Expand Up @@ -115,8 +115,6 @@ internal class JUnit5RobolectricTestRunner(
}

internal companion object {
private val beforeTestLock = Any()

private fun defaultInjectorBuilder() =
defaultInjector().bind(SandboxBuilder::class.java, JUnit5RobolectricSandboxBuilder::class.java)
.bind(MavenDependencyResolver::class.java, JUnit5MavenDependencyResolver::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ internal class JUnit5RobolectricTestRunnerHelper private constructor(testClass:
runWithRobolectric {
robolectricTestRunner.runAfterTest(frameworkMethod, testMethod)
}
robolectricTestRunner.runFinallyAfterTest(frameworkMethod)
robolectricTestRunner.runFinallyAfterTest(this, frameworkMethod)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,34 @@ internal fun Sandbox.resetClassLoaderToOriginal() {
}
Thread.currentThread().contextClassLoader = robolectricClassLoader.parent
}

internal fun Sandbox.clearShadowLooperCache() {
val shadowLooperClass = robolectricClassLoader.loadClass("org.robolectric.shadows.ShadowLooper")
shadowLooperClass.getDeclaredMethod("clearLooperMode").invoke(null)
}

internal fun Sandbox.resetLooper() {
resetMainLooper()
resetMyLooper()
}

private fun Sandbox.resetMainLooper() {
val looperClass = robolectricClassLoader.loadClass("android.os.Looper")

@Suppress("DiscouragedPrivateApi")
val sMainLooperField = looperClass.getDeclaredField("sMainLooper")
sMainLooperField.isAccessible = true
sMainLooperField.set(null, null)
sMainLooperField.isAccessible = false
}

private fun Sandbox.resetMyLooper() {
val looperClass = robolectricClassLoader.loadClass("android.os.Looper")

@Suppress("DiscouragedPrivateApi")
val sThreadLocalField = looperClass.getDeclaredField("sThreadLocal")
sThreadLocalField.isAccessible = true
val threadLocal = (sThreadLocalField.get(null) as ThreadLocal<*>)
threadLocal.remove()
sThreadLocalField.isAccessible = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
import org.robolectric.annotation.LooperMode
import tech.apter.junit.jupiter.robolectric.internal.extensions.isJUnit5NestedTest

internal object TestClassValidator {
Expand All @@ -16,7 +15,6 @@ internal object TestClassValidator {
validateNestedTestClassCanNotOverrideRuntimeSdk(testClass)
validateNestedTestClassCanNotApplyAnnotations(
testClass,
LooperMode::class.java,
GraphicsMode::class.java,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package tech.apter.junit.jupiter.robolectric

import android.os.Looper
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.TestMethodOrder
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode
import org.robolectric.annotation.LooperMode
import kotlin.test.Test
import kotlin.test.assertNotSame
import kotlin.test.assertSame

@ExtendWith(RobolectricExtension::class)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
@Execution(ExecutionMode.SAME_THREAD)
class RobolectricExtensionLooperModeSelfTest {
@Test
@Order(1)
fun `Given a test method with default looper mode then testMethod should be invoked on the main thread`() {
assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread)
}

@Test
@Order(2)
@LooperMode(LooperMode.Mode.PAUSED)
fun `Given a test method with looper mode paused then testMethod should be invoked on the main thread`() {
assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread)
}

@Test
@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST)
@Order(3)
fun `Given a test method with looper mode instrumentation test when called after a paused test method then testMethod should be not invoked on the main thread`() {
assertNotSame(Thread.currentThread(), Looper.getMainLooper()?.thread)
}

@Test
@Order(4)
@LooperMode(LooperMode.Mode.PAUSED)
fun `Given a test method with looper mode paused when called after an instrumentation test method then testMethod should be invoked on the main thread`() {
assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread)
}

@Nested
@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
@DisplayName("Given a nested test class with looper mode instrumentation test")
inner class NestedInstrumentationTest {

@Test
@LooperMode(LooperMode.Mode.PAUSED)
@Order(1)
fun `and a test method with looper mode paused then testMethod should be invoked on the main thread`() {
assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread)
}

@Test
@Order(2)
fun `and a test method with looper with default looper mode set after paused test then testMethod should be not invoked on the main thread`() {
assertNotSame(Thread.currentThread(), Looper.getMainLooper()?.thread)
}
}

@Nested
@LooperMode(LooperMode.Mode.PAUSED)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
@DisplayName("Given a nested test class with looper mode paused")
inner class NestedPausedTest {
@Test
fun `and a test method with looper with default looper mode set then testMethod should be not invoked on the main thread`() {
assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread)
}

@Test
@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST)
fun `and a test method with looper mode instrumentation test when call after a paused test then testMethod should be not invoked on the main thread`() {
assertNotSame(Thread.currentThread(), Looper.getMainLooper()?.thread)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,6 @@ class RobolectricExtensionSelfTest {
assertContains(RuntimeEnvironment.getQualifiers(), "en")
}

@Test
fun testMethod_shouldBeInvoked_onMainThread() {
assertSame(Thread.currentThread(), Looper.getMainLooper().thread)
}

@Test
@Timeout(1000)
fun whenTestHarnessUsesDifferentThread_shouldStillReportAsMainThread() {
Expand Down