https://kotlinlang.org logo
#arrow
Title
# arrow
r

Riccardo Cardin

10/14/2023, 9:18 AM
Hi, all. I’m trying to develop a test of an application using Arrow Raise DSL arrow. In detail, I want to use Mockk to mock some dependencies. I know I can avoid mocks, but I want to understand how to test them. I have the following classes:
Copy code
interface IB {
    context (Raise<Error>)
    fun b(): Int
}

class B : IB {
    context(Raise<Error>)
    override fun b(): Int {
        return 42
    }
}

interface IA {
    context (Raise<Error>)
    fun a(): Int
}

class A(private val b: IB) : IA {
    context(Raise<Error>)
    override fun a(): Int {
        return b.b()
    }

}

class Error
The test looks like the following:
Copy code
internal class MockkArrowRaiseTest {

    @Test
    internal fun test1() {
        val b = mockk<IB>()
        val a = A(b)
        every {
            with(any<Raise<Error>>()) {
                b.b()
            }
        } returns 42

        fold(
            block = { a.a() },
            recover = { _: Error -> assert(false) },
            transform = { assert(it == 42) }
        )
    }
}
My question is: is there a better way to test such code or a better solution (from the pov of simplicity 📖, maintainability 🔧, etc…)?
arrow intensifies 4
neat 1
s

simon.vergauwen

10/14/2023, 9:51 AM
Hey @Riccardo Cardin, I recently had a discussion on this with @Olaf Gottschalk, and this seems like a pretty good solution to me! I would probably hide the nested `every`/`with` into a utility function.
It would probably be good to document this on the website, I'm sure more people using Mockk will run into this
👍 1
r

Riccardo Cardin

10/14/2023, 11:11 AM
Is the fold for assertions correct?
s

simon.vergauwen

10/14/2023, 11:26 AM
It's definitely correct, but not sure if it's the simplest, or most maintainable way 😅 There are several ways of dealing with it, thanks to
Raise
being so flexible. You could also do
assert(either { a.a() } == Right(42))
or
assert(either { a.a() }.getOrNull() == 42)
. That is probably my favorite approach. But otherwise I'd probably recommend defining a custom assertion of
Raise
instead of repeating
fold
everywhere
assertSucess(42) { a.a() }
which gives a nice error message instead of
{ _: Error -> assert(false) }
.
gratitude thank you 1
o

Olaf Gottschalk

10/14/2023, 11:30 AM
I can show you what I came up with on Monday 😉
❤️ 1
mind blown 1
K 2
r

Riccardo Cardin

10/16/2023, 10:09 AM
@Olaf Gottschalk , any update on this? ☺️
o

Olaf Gottschalk

10/16/2023, 4:20 PM
Hey! I fought a lot today with compiler errors from Kotlin when trying to make my specific use case more generic for you. Some things are really, really not production stable with context receivers yet.
Copy code
org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during psi2ir
File being compiled: (194,18) in /Users/q163557/repos/jagathe-gradle/src/test/kotlin/com/bmw/otd/agathe/sap/RfcSinkTest.kt
The root cause java.lang.IllegalStateException was thrown at: org.jetbrains.kotlin.psi2ir.generators.ArgumentsGenerationUtilsKt.generateReceiver(ArgumentsGenerationUtils.kt:98)
Unknown receiver: Cxt { context(arrow.core.raise.Raise<kotlin.String>, io.mockk.MockKMatcherScope) local final fun `<anonymous>`(): <http://kotlin.Int|kotlin.Int> defined in com.bmw.otd.agathe.sap.RfcSinkTest.testing[AnonymousFunctionDescriptor@710f76f3] }: KtDotQualifiedExpression:
myMock.bar(any())
So, I do not want to bother you with that, but I fear I cannot contribute as much as I wanted in the generic case here. My situation was regarding setting up a mock of a function type and get an equivalent for
Copy code
every { foo() } throws IllegalArgumentException("boom")
So, my goal was to have something similar for this specific use case:
Copy code
val mockedFun: Raise<String>.(Int) -> Unit = mockk()
every { mockedFun(any(), any()) } raises "Boom"
Here you can already see one problem: for mockk to mock the function call with a receiver, you have to give both the receiver AND the single argument together as two parameters. The first
any()
denotes the Raise context, the second the
Int
argument. The code behind this looks like this:
Copy code
infix fun <T, B, Error> MockKStubScope<T, B>.raises(r: Error): MockKAdditionalAnswerScope<T, B> = answers {
    withSingleRaiseScopeOrFail { raise(r) }
}

infix fun <T, B, Error> MockKAdditionalAnswerScope<T, B>.andThenRaises(r: Error) = andThenAnswer {
    withSingleRaiseScopeOrFail { raise(r) }
}


private fun <T, B, Error> MockKAnswerScope<T, B>.withSingleRaiseScopeOrFail(block: Raise<Error>.() -> T) =
    with(findSingleRaiseDslArgument<Error>()) { block() }

@Suppress("UNCHECKED_CAST")
private fun <Error> MockKAnswerScope<*, *>.findSingleRaiseDslArgument(): Raise<Error> =
    (args.singleOrNull { it is Raise<*> }
        ?: error("None or multiple Raise arguments have been found!")) as Raise<Error>
The problem when declaring the answer of the function now is that due to type erasure it is not possible to ensure the Raise context found by
findSingleRaiseDslArgument
is really the correct one for cases where technically you could have multiple receivers for different
Raise
contexts... So my solution was to restrict this to work ONLY if there is one single Raise argument - not more, not less. That one is type cast to the correct error type. I see there is quite some work ahead, especially to make mockk understand context receivers fully. But for my specific situation, this was enough. Another helper fun I have create was the equivalent for
shouldThrow
and
shouldNotThrow
.Their implementations look like this:
Copy code
inline fun <E, T> shouldRaise(block: Raise<E>.() -> T): E =
    fold(block, { throw it }, { it }, { fail("Expected raised error, but nothing was raised.") })

inline fun <E, T> shouldNotRaise(block: Raise<E>.() -> T): T =
    fold(block, { throw it }, { fail("No raised error expected, but $it was raised.") }, { it })
This would go into the Kotest Arrow library. Hope it helps!?
👍 1
r

Riccardo Cardin

10/16/2023, 4:48 PM
Thanks a lot. Really appreciated
mind blown
@Olaf Gottschalk, I understand your struggle. I’m trying to add the support to the
Raise<E>
on my
assertj-arrow-core
library and I found the same error as you.
arrow intensifies 1
Btw, using a method reference instead of a lambda solves the issue:
Copy code
@Test
fun `should pass if lambda succeeds with the given value`() {
    assertThat(this::aFunctionWithContextThatSucceeds).succeedsWith(42)
}
2 Views