Skip to content

JUnit integration

The testing module provides drop-in wrappers that own a per-test ComposeAutomator.

JUnit 5: ComposeAutomatorExtension

The safest pattern is @RegisterExtension on a @JvmField — one extension instance per test class, owned by the test class:

import dev.sebastiano.spectre.testing.ComposeAutomatorExtension
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

class MyTest {

    @JvmField
    @RegisterExtension
    val automatorExt = ComposeAutomatorExtension()

    @Test
    fun something() {
        val node = automatorExt.automator.findOneByTestTag("Send")
        // ...
    }
}

The extension also implements ParameterResolver, so you can use @ExtendWith and take the automator as a parameter:

import dev.sebastiano.spectre.core.ComposeAutomator
import dev.sebastiano.spectre.testing.ComposeAutomatorExtension
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(ComposeAutomatorExtension::class)
class MyTest {

    @Test
    fun something(automator: ComposeAutomator) {
        val node = automator.findOneByTestTag("Send")
        // ...
    }
}

Parallel execution

The parameter-injection form is the parallel-safe form: each test resolves its own automator from the per-invocation ExtensionContext.Store. The automatorExt.automator accessor returns the most recently created instance and is fine for sequential runs but races under parallel execution.

JUnit 4: ComposeAutomatorRule

import dev.sebastiano.spectre.testing.ComposeAutomatorRule
import org.junit.Rule
import org.junit.Test

class MyTest {

    @get:Rule
    val automatorRule = ComposeAutomatorRule()

    @Test
    fun something() {
        val node = automatorRule.automator.findOneByTestTag("Send")
        // ...
    }
}

@get:Rule (note the get: prefix) targets the annotation at the property's generated getter, which is what JUnit 4 reflects on. Without the get: prefix Kotlin would put the annotation on the property itself and JUnit wouldn't see it.

Custom AutomatorFactory

Both wrappers default to ComposeAutomator.inProcess(). Pass your own factory when you need a different driver for headless CI or unit-style isolation. RobotDriver.headless() throws on input, clipboard, and screenshot calls (see Driving input), so the example below is appropriate for tests that only exercise semantics-tree queries or rule/extension lifecycle — anything that needs real input should use RobotDriver.synthetic(rootWindow) or the default RobotDriver() instead:

import dev.sebastiano.spectre.core.ComposeAutomator
import dev.sebastiano.spectre.core.RobotDriver
import dev.sebastiano.spectre.testing.AutomatorFactory
import dev.sebastiano.spectre.testing.ComposeAutomatorExtension
import org.junit.jupiter.api.extension.RegisterExtension

private val headlessFactory: AutomatorFactory = {
    ComposeAutomator.inProcess(robotDriver = RobotDriver.headless())
}

class HeadlessTest {

    @JvmField
    @RegisterExtension
    val automatorExt = ComposeAutomatorExtension(headlessFactory)
}

JUnit dependency model

Both junit:junit (JUnit 4) and org.junit.jupiter:junit-jupiter-api (JUnit 5) are declared compileOnly on the testing module. Consumers pick whichever JUnit they already use and pull in the matching test dependency themselves. The module never forces both onto the test classpath.

If you see a NoClassDefFoundError for a JUnit class when the rule or extension runs, add the corresponding testImplementation dependency to your project — see Installation.