https://kotlinlang.org logo
Title
c

Carlton Whitehead

10/09/2021, 8:05 PM
I'm having some difficulty setting up unit tests for my subcommands which use
requireObject()
to pull in state from parent commands. Ideally, I'd like to install these through the context builder. Example code in thread.
class Subcommand : CliktCommand() {

    private val environment: Environment by requireObject()
    
    override fun run() {
        echo(environment.value)
    }
}

class SubcommandTest : CliktCommand() {
    
    lateinit var subcommand: Subcommand
    
    lateinit var testConsole: StringBufferConsole
    
    @BeforeEach
    fun beforeEach() {
        testConsole = StringBufferConsole()
        subcommand = Subcommand()
            .context {
                console = testConsole
                obj = Environment(value = "test")
                // but there is no `obj` available in Context.Builder
            }
    }
    
    @Test
    fun `It should echo environment value`() {
        subcommand.parse(emptyArray())
        
        assertThat(testConsole.output).contains("test")
    }
}
I tried setting it through
subcommand.currentContext.obj
during
beforeEach()
, but accessing the context prior to parse is not allowed. For the time being, I've resorted to adding constructor parameters to each of my subcommands to allow the unit tests to override the environment.
class SubcommandTest(forceEnvironment: Environment? = null) : CliktCommand() {
    private val environment: Environment by requireObject()
    
    override fun run() {
        val useEnvironment = forceEnvironment ?: environment
        echo(useEnvironment.value)
    }
}
I'm not too fond of this approach because it adds complexity to main code simply for test concerns. It feels like a missing feature that there isn't a way to pass an
obj
value into the context from the context builder. Have I missed something here? Is there a better way I should approach this instead?
a

AJ Alt

10/10/2021, 4:13 PM
Given that your class is expected to be a subcommand, I would suggest testing it as one:
class Parent : CliktCommand() {
    override fun run() {
        currentContext.obj = Environment("test")
    }
}

@Test
fun `It should echo environment value`() {
    Parent().context { console = testConsole }
        .subcommands(Subcommand())
        .parse(emptyArray())

    assertThat(testConsole.output).contains("test")
}
Although adding an
obj
setter to the context builder also seems like a reasonable feature.
c

Carlton Whitehead

10/10/2021, 5:02 PM
That seems like a practical compromise between completely isolating the unit under test (I have quite a preference towards this), the coupling that exists due to the command/subcommand relationship, and the limitation of the framework. In the case of an app with multiple nesting levels of commands which each contribute
obj
values, I could see this being a preferred approach. Another option I've since tried is a singleton model. Write the value in the root command, and then read the value in subcommands. This leads to quite a lot of constructor boilerplate throughout an app with many subcommands but otherwise works out pretty nicely for my use case. My preference remains for there to be an
obj
available in the
Context.Builder
. I'll send you a PR adding support for that.