-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(runner): Add Robolectric test runner helper (#9)
- Loading branch information
Showing
7 changed files
with
307 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
...kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunnerHelper.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package tech.apter.junit.jupiter.robolectric.internal | ||
|
||
import com.google.common.annotations.VisibleForTesting | ||
import java.lang.reflect.Method | ||
import org.junit.runners.model.FrameworkMethod | ||
import org.robolectric.internal.bytecode.Sandbox | ||
|
||
internal class JUnit5RobolectricTestRunnerHelper { | ||
private inline val logger get() = createLogger() | ||
private var _robolectricTestRunner: JUnit5RobolectricTestRunner? = null | ||
private var _sdkEnvironment: Sandbox? = null | ||
private var _frameworkMethod: FrameworkMethod? = null | ||
|
||
@VisibleForTesting | ||
val robolectricTestRunner: JUnit5RobolectricTestRunner get() = requireNotNull(_robolectricTestRunner) | ||
val sdkEnvironment: Sandbox get() = requireNotNull(_sdkEnvironment) | ||
|
||
@VisibleForTesting | ||
val frameworkMethod: FrameworkMethod get() = requireNotNull(_frameworkMethod) | ||
|
||
fun loadRobolectricClassLoader() { | ||
logger.trace { "loadRobolectricClassLoader" } | ||
Thread.currentThread().replaceClassLoader(sdkEnvironment.robolectricClassLoader).also { | ||
if (interceptedClassLoader == null) { | ||
interceptedClassLoader = it | ||
} | ||
} | ||
} | ||
|
||
fun resetClassLoaderToOriginal() { | ||
logger.trace { "resetClassLoaderToOriginal" } | ||
if (interceptedClassLoader != null) { | ||
Thread.currentThread().contextClassLoader = interceptedClassLoader | ||
} | ||
} | ||
|
||
fun createTestEnvironmentForClass(testClass: Class<*>) { | ||
_robolectricTestRunner = JUnit5RobolectricTestRunner(testClass) | ||
_sdkEnvironment = robolectricTestRunner.bootstrapSdkEnvironment() | ||
} | ||
|
||
fun createTestEnvironmentForMethod(testMethod: Method) { | ||
val frameworkMethod = robolectricTestRunner.frameworkMethod(testMethod) | ||
_sdkEnvironment = robolectricTestRunner.sdkEnvironment(frameworkMethod) | ||
_frameworkMethod = frameworkMethod | ||
} | ||
|
||
fun beforeEach(testMethod: Method) { | ||
robolectricTestRunner.runBeforeTest(sdkEnvironment, frameworkMethod, testMethod) | ||
} | ||
|
||
fun afterEach(testMethod: Method) { | ||
robolectricTestRunner.runAfterTest(frameworkMethod, testMethod) | ||
} | ||
|
||
fun afterEachFinally() { | ||
robolectricTestRunner.runFinallyAfterTest(frameworkMethod) | ||
} | ||
|
||
fun clearCachedRobolectricTestRunnerEnvironment() { | ||
_robolectricTestRunner = null | ||
_sdkEnvironment = null | ||
_frameworkMethod = null | ||
} | ||
|
||
internal companion object { | ||
internal var interceptedClassLoader: ClassLoader? = null | ||
private set | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
...h/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunnerHelperExtensions.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package tech.apter.junit.jupiter.robolectric.internal | ||
|
||
import java.util.concurrent.Callable | ||
|
||
internal fun <T> JUnit5RobolectricTestRunnerHelper.runOnMainThread(action: JUnit5RobolectricTestRunnerHelper.() -> T): T = | ||
sdkEnvironment.runOnMainThread( | ||
Callable { | ||
action() | ||
} | ||
) | ||
|
||
internal fun <T> JUnit5RobolectricTestRunnerHelper.runOnMainThreadWithRobolectric(action: JUnit5RobolectricTestRunnerHelper.() -> T): T = | ||
runOnMainThread { | ||
runWithRobolectric(action) | ||
} | ||
|
||
internal fun <T> JUnit5RobolectricTestRunnerHelper.runWithRobolectric(action: JUnit5RobolectricTestRunnerHelper.() -> T): T { | ||
loadRobolectricClassLoader() | ||
return action().also { | ||
resetClassLoaderToOriginal() | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
...tension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/ThreadExtensions.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package tech.apter.junit.jupiter.robolectric.internal | ||
|
||
internal fun Thread.replaceClassLoader(classLoader: ClassLoader?): ClassLoader? { | ||
val originalClassLoader = contextClassLoader | ||
contextClassLoader = classLoader | ||
return originalClassLoader | ||
} |
171 changes: 171 additions & 0 deletions
171
...src/test/kotlin/tech/apter/junit/jupiter/robolectric/JUnit5RobolectricRunnerHelperTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
package tech.apter.junit.jupiter.robolectric | ||
|
||
import org.junit.jupiter.api.assertDoesNotThrow | ||
import org.junit.jupiter.api.assertThrows | ||
import org.robolectric.internal.AndroidSandbox.SdkSandboxClassLoader | ||
import org.robolectric.internal.bytecode.Sandbox | ||
import tech.apter.junit.jupiter.robolectric.internal.JUnit5RobolectricTestRunnerHelper | ||
import tech.apter.junit.jupiter.robolectric.internal.fakes.SingleTestMethodJunitJupiterTest | ||
import tech.apter.junit.jupiter.robolectric.internal.fakes.TwoTestMethodsJunitJupiterTest | ||
import kotlin.test.AfterTest | ||
import kotlin.test.BeforeTest | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertNotEquals | ||
import kotlin.test.assertNotSame | ||
import kotlin.test.assertSame | ||
|
||
class JUnit5RobolectricRunnerHelperTest { | ||
|
||
private var originalClassLoader: ClassLoader? = null | ||
|
||
@BeforeTest | ||
fun setUp() { | ||
originalClassLoader = JUnit5RobolectricTestRunnerHelper.interceptedClassLoader | ||
} | ||
|
||
@AfterTest | ||
fun tearDown() { | ||
Thread.currentThread().contextClassLoader = originalClassLoader | ||
originalClassLoader = null | ||
} | ||
|
||
@Test | ||
fun `When call loadRobolectricClassLoader then contextClassLoader should be an instance of SdkSandboxClassLoader`() { | ||
subjectUnderTest { | ||
// Given | ||
createTestEnvironmentForClass(SingleTestMethodJunitJupiterTest::class.java) | ||
|
||
// When | ||
loadRobolectricClassLoader() | ||
|
||
// Then | ||
assertEquals<Class<*>>( | ||
SdkSandboxClassLoader::class.java, | ||
Thread.currentThread().contextClassLoader.javaClass | ||
) | ||
} | ||
} | ||
|
||
@Test | ||
fun `Given the robolectricClassLoader loaded when call reset resetClassLoaderToOriginal then contextClassLoader should not be an instance of SdkSandboxClassLoader`() { | ||
subjectUnderTest { | ||
// Given | ||
createTestEnvironmentForClass(SingleTestMethodJunitJupiterTest::class.java) | ||
loadRobolectricClassLoader() | ||
|
||
// When | ||
resetClassLoaderToOriginal() | ||
|
||
// Then | ||
val currentClassLoader = Thread.currentThread().contextClassLoader | ||
assertNotEquals<Class<*>>(SdkSandboxClassLoader::class.java, currentClassLoader.javaClass) | ||
assertSame(originalClassLoader, currentClassLoader) | ||
} | ||
} | ||
|
||
@Test | ||
fun `Given a configured test environment when call reset resetClassLoaderToOriginal then contextClassLoader should be the same as original`() { | ||
subjectUnderTest { | ||
// Given | ||
createTestEnvironmentForClass(SingleTestMethodJunitJupiterTest::class.java) | ||
|
||
// When | ||
resetClassLoaderToOriginal() | ||
|
||
// Then | ||
assertSame(originalClassLoader, Thread.currentThread().contextClassLoader) | ||
} | ||
} | ||
|
||
@Test | ||
fun `When call reset resetClassLoaderToOriginal then contextClassLoader should be the same as original`() { | ||
subjectUnderTest { | ||
// When | ||
resetClassLoaderToOriginal() | ||
|
||
// Then | ||
assertSame(originalClassLoader, Thread.currentThread().contextClassLoader) | ||
} | ||
} | ||
|
||
@Test | ||
fun `Initially the test runner helper should be empty`() { | ||
subjectUnderTest { | ||
// Then | ||
assertThrows<IllegalArgumentException> { robolectricTestRunner } | ||
assertThrows<IllegalArgumentException> { sdkEnvironment } | ||
assertThrows<IllegalArgumentException> { frameworkMethod } | ||
} | ||
} | ||
|
||
@Test | ||
fun `Given a testRunner when call configure with runner the sdk environment should be set`() { | ||
// Given | ||
val cache = subjectUnderTest { | ||
// When | ||
createTestEnvironmentForClass(TwoTestMethodsJunitJupiterTest::class.java) | ||
} | ||
|
||
// Then | ||
assertDoesNotThrow { cache.robolectricTestRunner } | ||
assertEquals(TwoTestMethodsJunitJupiterTest::class.java, cache.robolectricTestRunner.testClass.javaClass) | ||
assertDoesNotThrow { cache.sdkEnvironment } | ||
assertThrows<IllegalArgumentException> { cache.frameworkMethod } | ||
} | ||
|
||
@Test | ||
fun `Given the sdk environment configured when call configure with framework method then sdk environment should be reconfigured`() { | ||
// Given | ||
val testClass = TwoTestMethodsJunitJupiterTest::class.java | ||
val testMethod2 = testClass.declaredMethods.first { | ||
it.name == TwoTestMethodsJunitJupiterTest::testMethod2.name | ||
} | ||
lateinit var firstSdkEnvironment: Sandbox | ||
|
||
val runnerHelper = subjectUnderTest { | ||
// And | ||
createTestEnvironmentForClass(testClass) | ||
firstSdkEnvironment = sdkEnvironment | ||
|
||
// When | ||
createTestEnvironmentForMethod(testMethod2) | ||
} | ||
|
||
// Then | ||
assertDoesNotThrow { runnerHelper.robolectricTestRunner } | ||
assertEquals(testClass, runnerHelper.robolectricTestRunner.testClass.javaClass) | ||
assertDoesNotThrow { runnerHelper.sdkEnvironment } | ||
assertNotSame(firstSdkEnvironment, runnerHelper.sdkEnvironment) | ||
assertDoesNotThrow { (runnerHelper.frameworkMethod) } | ||
assertEquals(TwoTestMethodsJunitJupiterTest::testMethod2.name, runnerHelper.frameworkMethod.name) | ||
} | ||
|
||
@Test | ||
fun `Given the runnerHelper configured when call clear then the runnerHelper should be empty`() { | ||
// Given | ||
val testClass = TwoTestMethodsJunitJupiterTest::class.java | ||
val testMethod2 = testClass.declaredMethods.first { | ||
it.name == TwoTestMethodsJunitJupiterTest::testMethod2.name | ||
} | ||
val runnerHelper = subjectUnderTest { | ||
|
||
// And | ||
createTestEnvironmentForClass(testClass) | ||
|
||
// And | ||
createTestEnvironmentForMethod(testMethod2) | ||
|
||
// When | ||
clearCachedRobolectricTestRunnerEnvironment() | ||
} | ||
|
||
// Then | ||
assertThrows<IllegalArgumentException> { runnerHelper.robolectricTestRunner } | ||
assertThrows<IllegalArgumentException> { runnerHelper.sdkEnvironment } | ||
assertThrows<IllegalArgumentException> { runnerHelper.frameworkMethod } | ||
} | ||
|
||
private fun subjectUnderTest(action: JUnit5RobolectricTestRunnerHelper.() -> Unit): JUnit5RobolectricTestRunnerHelper = | ||
JUnit5RobolectricTestRunnerHelper().apply(action) | ||
} |
15 changes: 15 additions & 0 deletions
15
...lin/tech/apter/junit/jupiter/robolectric/internal/fakes/TwoTestMethodsJunitJupiterTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package tech.apter.junit.jupiter.robolectric.internal.fakes | ||
|
||
import org.junit.jupiter.api.Disabled | ||
import org.junit.jupiter.api.Test | ||
import org.robolectric.annotation.Config | ||
|
||
@Disabled | ||
class TwoTestMethodsJunitJupiterTest { | ||
@Test | ||
fun testMethod1() = Unit | ||
|
||
@Test | ||
@Config(minSdk = 33) | ||
fun testMethod2() = Unit | ||
} |