Can I access variable definitions in one block from another one?

There is this “arrange-act-assert” pattern for tests, and I asked myself if it would be possible to write a DSL separating the three phases, something like

arrange {
    val x = 23
    val y = 15
}.act {
    x + y
}.asserting {
    assertThat(it).equals(38)
}

I found no way to get x and y variables from the first block into scope of the second. Is this even possible?

This might not be the most useful example, but I think the ability to pass around variable references captured in a block could be a powerful tool for DSLs.

Pretty sure that’s not possible. Local variables are scoped to the block they’re declared in.

3 Likes

Yeah this is a Java limitation; variables are scoped to the block you define them in.

If you wanted to get a little bit crazy, you could use Strings and some custom functions to get something like that.

Off the top of my head, something like this:

arrange {
    "x" hasValue 23
    "y" hasValue 15
}.act {
    "x" + "y"
}.asserting {
    assertThat(it).equals(38)
}

Assuming arrange takes a lambda that acts on instance of Arranger or something:

class Arranger {
    internal val values = mutableMapOf<String, Any>()

    infix fun String.hasValue(value: Any) {
        values[this] = value
    }
}

Then for whatever class your act lambda operates on, it can access the values field from the Arranger class somehow… not sure if you’d need to provide a custom function for it to run, or something?

class Act(private val values: Map<String, Any>) {
    infix operator fun String.plus(other: String): Int {
        val firstValue = values[this] ?: throw IllegalStateException("No value $this registered in test setup")
        val secondValue = values[other]  ?: throw IllegalStateException("No value $other registered in test setup")

        if (firstValue !is Int) throw IllegalStateException("Value provided for $this is not Int")
        if (secondValue !is Int) throw IllegalStateException("Value provided for $other is not Int")

        return firstValue + secondValue
    }
}

Obviously the second part isn’t exactly feasible for every test… your Act class can’t have function definitions for every operation you want to do in your tests, so you might need a way of providing custom function definitions to the Act class or something… not sure. We could iterate on that.

If you like this idea so far, give it a go.

I asked myself if it would be possible to write a DSL separating the three phases

What would be the benefit of this, as opposed to writing things normally? It looks like it just adds a layer of complexity for future readers, and actually breaks the most simple things as you have just shown.

This is the closest you can get to I think. Using an anonymous object:

fun main() {
    arrange(object {
        val x = 23
        val y = 15
    }).act {
        x + y
    }.asserting {
        require(it == 38)
    }
}

inline fun <T> arrange(obj: T) = obj

inline fun <T, R> T.act(block: T.() -> R) = block()
inline fun <T> T.asserting(block: (T) -> Unit) = block(this)
1 Like

Thank you, I like it! That’s a very creative solution.

I think in this context, there is no real upside in using a DSL (maybe if you want to time or log or error-catch specifically the act phase?), but it’s a really neat trick which could be useful for less contrived cases.

1 Like