Riccardo Cardin
10/14/2023, 9:18 AMinterface 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:
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…)?simon.vergauwen
10/14/2023, 9:51 AMRiccardo Cardin
10/14/2023, 11:11 AMsimon.vergauwen
10/14/2023, 11:26 AMRaise
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) }
.Olaf Gottschalk
10/14/2023, 11:30 AMRiccardo Cardin
10/16/2023, 10:09 AMOlaf Gottschalk
10/16/2023, 4:20 PMorg.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
every { foo() } throws IllegalArgumentException("boom")
So, my goal was to have something similar for this specific use case:
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:
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:
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!?Riccardo Cardin
10/16/2023, 4:48 PMRaise<E>
on my assertj-arrow-core
library and I found the same error as you.@Test
fun `should pass if lambda succeeds with the given value`() {
assertThat(this::aFunctionWithContextThatSucceeds).succeedsWith(42)
}