How to Store Values in JUnit 5 Extensions and Inject in Parameterized Test

Issue

Overview

Expected – Create a JUnit 5 Extension class in order to manage use of a TestCoroutineDispatcher.

Observed – Unable to access the testDispatcher variable created within the Extension class.

Extension Implementation

Test.kt

@ExtendWith(InstantExecutorExtension::class, MainCoroutineExtension::class)
class FeedLoadContentTests {
    private val contentViewModel  ContentViewModel()
    private fun FeedLoad()  feedLoadTestCases()

    @ParameterizedTest
    @MethodSource("FeedLoad")
    @ExtendWith(MainCoroutineExtension::class)
    fun `Feed Load`(test: FeedLoadContentTest)  testDispatcher.runBlockingTest {
        // Some testing done here.
    }
}

Extension.kt

class MainCoroutineExtension : BeforeEachCallback, AfterEachCallback {
    val testDispatcher  TestCoroutineDispatcher()

    override fun beforeEach(context: ExtensionContext?) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
}

Solution

Here are three implementations that work in theory. However, the last solution is the best, Store Extension Values with getStore and Inject Parameters using ParameterResolver, because it ensures lifecycle safety.

Thank you to @johanneslink, for guiding me in the right direction!

Programmatic Extension Registration

Strategy

TLDR – Use a Programmatic Extension Registration.

This strategy works as expected with the TestCoroutineDispatcher created in the MainCoroutineExtension, and its’ lifecycle managed with the test lifecycle implementations.

Implementation

Test.kt

class FeedLoadContentTests {

    companion object {
        @JvmField
        @RegisterExtension
        val mainCoroutineExtension  MainCoroutineExtension()
    }

    private val contentViewModel  ContentViewModel()
    private fun FeedLoad()  feedLoadTestCases()

    @ParameterizedTest
    @MethodSource("FeedLoad")
    @ExtendWith(MainCoroutineExtension::class)
    fun `Feed Load`(test: FeedLoadContentTest)  
        mainCoroutineExtension.testDispatcher.runBlockingTest {
        // Some testing done here.
        }
}

Extension.kt

class MainCoroutineExtension : BeforeEachCallback, AfterEachCallback {
    val testDispatcher  TestCoroutineDispatcher()

    override fun beforeEach(context: ExtensionContext?) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
}

Inject Parameters using ParameterResolver

Strategy

TLDR – Use a ParameterResolver.

This approach implements a ParameterResolver in order to inject a TestCoroutineDispatcher required to manage the Coroutine lifecycle in local JUnit test.

Implementation

Test.kt

@ExtendWith(LifecycleExtensions::class)
// The TestCoroutineDispatcher is injected here as a parameter.
class FeedLoadContentTests(val testDispatcher: TestCoroutineDispatcher) {

    private val contentViewModel  ContentViewModel()
    private fun FeedLoad()  feedLoadTestCases()

    @ParameterizedTest
    @MethodSource("FeedLoad")
    fun `Feed Load`(test: FeedLoadContentTest)  testDispatcher.runBlockingTest {
        // Some testing done here.
    }
}

Extension.kt

class LifecycleExtensions :  BeforeEachCallback, AfterEachCallback, ParameterResolver {

    val testDispatcher  TestCoroutineDispatcher()

    override fun beforeEach(context: ExtensionContext?) {
        // Set Coroutine Dispatcher.
        Dispatchers.setMain(testDispatcher)
        ...
    }

    override fun afterEach(context: ExtensionContext?) {
        // Reset Coroutine Dispatcher.
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
        ...
    }

    override fun supportsParameter(parameterContext: ParameterContext?,
                                   extensionContext: ExtensionContext?) 
            parameterContext?.parameter?.type  TestCoroutineDispatcher::class.java

    override fun resolveParameter(parameterContext: ParameterContext?,
                                  extensionContext: ExtensionContext?) 
            testDispatcher

}

Store Extension Values with getStore and Inject Parameters using ParameterResolver

The only refactor here that is different from Inject Parameters using ParameterResolver above, is using getStore to store the TestCoroutineDispatcher. It is important that context?.root is used in order to avoid creating multiple instances of the injected value per Test class.

This is instead of storing TestCoroutineDispatcher as a member variable, which can lead to lifecycle issues when running tests in parallel.

Extension.kt

class LifecycleExtensions : BeforeAllCallback, AfterAllCallback, BeforeEachCallback,
        AfterEachCallback, ParameterResolver {
    ...

    override fun beforeEach(context: ExtensionContext?) {
        // Set Coroutine Dispatcher.
        Dispatchers.setMain(context?.root
                ?.getStore(STORE_NAMESPACE)
                ?.get(STORE_KEY, TestCoroutineDispatcher::class.java)!!)

        ...
    }

    override fun afterEach(context: ExtensionContext?) {
        // Reset Coroutine Dispatcher.
        Dispatchers.resetMain()
        context?.root
                ?.getStore(STORE_NAMESPACE)
                ?.get(STORE_KEY, TestCoroutineDispatcher::class.java)!!.cleanupTestCoroutines()

        ...
    }

    override fun supportsParameter(parameterContext: ParameterContext?,
                                   extensionContext: ExtensionContext?) 
            parameterContext?.parameter?.type  TestCoroutineDispatcher::class.java

    override fun resolveParameter(parameterContext: ParameterContext?,
                              extensionContext: ExtensionContext?) 
        getTestCoroutineDispatcher(extensionContext).let { dipatcher ->
            if (dipatcher  null) saveAndReturnTestCoroutineDispatcher(extensionContext)
            else dipatcher
        }

    private fun getTestCoroutineDispatcher(context: ExtensionContext?)  context?.root
        ?.getStore(TEST_COROUTINE_DISPATCHER_NAMESPACE)
        ?.get(TEST_COROUTINE_DISPATCHER_KEY, TestCoroutineDispatcher::class.java)

    private fun saveAndReturnTestCoroutineDispatcher(extensionContext: ExtensionContext?) 
        TestCoroutineDispatcher().apply {
            extensionContext?.root
                    ?.getStore(TEST_COROUTINE_DISPATCHER_NAMESPACE)
                    ?.put(TEST_COROUTINE_DISPATCHER_KEY, this)
        }

Answered By – Adam Hurwitz

Leave a Comment