New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: inline classes #152
Comments
Thanks for reporting it. |
|
And cannot capture an inline class using |
To everyone that might come back to this issue, I found the way to make this work. Before writing down the solution, I want to make clear the following points:
With this in mind, we can re-write the following tests: @Test // fails
fun `mockk test using any()`() {
val mocked = mockk<TestMock>(relaxed = true)
every { mocked.test(any()) } returns 1
} As the following @Test // works
fun `mockk test using any()`() {
val mocked = mockk<TestMock>(relaxed = true)
every { mocked.test(MyInlineType(any())) } returns 1
} This will work, as the I hope this is useful to everyone. @oleksiyp Probably this might be wrote down into the |
@RiccardoM that looks like a good workaround for the matching use case. What about the "method returns an For example: inline class TestMock(val value: String)
interface MyInterface {
fun returnsMock(): TestMock
} And tests: internal class MockkInlineTest {
@Test
internal fun `returns example`() {
val myInterface = mockk<MyInterface>()
every { myInterface.returnsMock() } returns TestMock("hello")
assertEquals(
TestMock("hello"),
myInterface.returnsMock()
)
}
@Test
internal fun `answers lambda example`() {
val myInterface = mockk<MyInterface>()
every { myInterface.returnsMock() } answers { TestMock("hello") }
assertEquals(
TestMock("hello"),
myInterface.returnsMock()
)
}
@Test
internal fun `answers implementation example`() {
val myInterface = mockk<MyInterface>()
val answer = object : Answer<TestMock> {
override fun answer(call: Call): TestMock {
return TestMock("hello")
}
}
every { myInterface.returnsMock() } answers(answer)
assertEquals(
TestMock("hello"),
myInterface.returnsMock()
)
}
} On Java 11 and Kotlin 1.3.31, all of these fail with a similar variant to:
In Mockito, What I have been unfortunately doing is creating fakes instead of utilizing Mockk in these places |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. If you are sure that this issue is important and should not be marked as |
I don't believe this should be closed (either is or other issue) - can somebody add the |
I use klock library which has an inline class at its core by design. I wanted to use mockk, but the lack of this feature/fix is a blocker to me to write unit tests. If mockk doesn't want to miss new clients and lose existing ones, I suggest providing a complete workaround or even ugly, experimental API for this, and introduce proper API/fix in implementation later. Otherwise I and probably a bunch of other people will have to look for another library. I understand that inline classes are an experimental feature of Kotlin and clients of libraries can expect undefined behavior in such case, but looks like authors of some libraries decide to rely on the experimental features really deeply and then it sort of forces other libraries to treat it in a "less experimental" way. @edit: I found a workaround for returning instances of inline classes, which is satisfactory for me for now. Instead of not working ( val timeProvider = mockk<TimeProvider>()
every { timeProvider.now() } returns DateTime.fromUnix(123) if you don't care about verifying the mock's call, use just a regular object, or if you do care, wrap it in a spy: val timeProvider = spyk<TimeProvider>(object : TimeProvider {
override fun now() = DateTime.fromUnix(123)
}) |
Thanks @krzema12 - your workaround worked for me and saved my day unfortunately it made the code quite ugly - so I would really like to see this fixed. |
I have the same problem, but all the workarounds here are focusing on returning inline classes. But I need to verify the call of method with inline class as it's parameter. inline class Group(val uuid: UUID) {
constructor(uuid: String) : this(UUID.fromString(uuid))
} And my test looks like this: private companion object {
private val GROUP_UUID = Group("4566900c-91b2-43cc-953e-28fe6354bf57")
}
@Test
fun testSomething() = runBlocking {
doSomething()
coVerify(inverse = true) { foo.bar(any()) }
} And this fails with:
The solution for
|
Proposed workaround doesn't work with
impossible. I've tried
but no luck. Any ideas? |
I'm also having troubles with |
I found a workaround for inline classes with inaccessible constructors like Define a function like this:
Usage:
|
@blazeroni A slightly more generic Workaround inline fun <reified T : Any, reified I : Any> MockKMatcherScope.anyInline() =
T::class.constructors.first()
.apply { isAccessible = true }
.call(any<I>()) |
@blazeroni a even more generic version, without the need to declare value type inline fun <reified T : Any> MockKMatcherScope.anyInline(): T {
val constructor = T::class.primaryConstructor!!
val valueType = constructor.parameters[0].type.classifier as KClass<*>
val any = (getProperty("callRecorder") as MockKGateway.CallRecorder)
.matcher(ConstantMatcher<T>(true), valueType)
return constructor.call(any)
} |
@blazeroni and finally a total replacement inline fun <reified T : Any> MockKMatcherScope.anyValue(): T =
if (T::class.isInline) anyInline()
else any()
inline fun <reified T : Any> MockKMatcherScope.anyInline(): T =
T::class.primaryConstructor!!.run {
val valueType = parameters[0].type.classifier as KClass<*>
call(match(ConstantMatcher<Any>(true), valueType))
}
fun <T : Any> MockKMatcherScope.match(matcher: Matcher<T>, type: KClass<T>): T =
(getProperty("callRecorder") as MockKGateway.CallRecorder).matcher(matcher, type)
val KClass<*>.isInline: Boolean
get() = !isData &&
primaryConstructor?.parameters?.size == 1 &&
java.declaredMethods.any { it.name == "box-impl" } |
however inline class as return values still not working :-/ |
finally I got it working even for return values see code below Usage Example for inline class kotlin.time.Duration mockk<Dummy> {
every { functionWithDurationParameter(anyValue()) } returns value(3.days)
} Mockk Extension Functions import io.mockk.ConstantMatcher
import io.mockk.MockKGateway.CallRecorder
import io.mockk.MockKMatcherScope
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
fun <T : Any> value(value: T): T =
if (value::class.isInline) inlineValue(value)
else value
inline fun <reified T : Any> MockKMatcherScope.anyValue(): T =
if (T::class.isInline) anyInlineValue()
else any()
@Suppress("UNCHECKED_CAST")
fun <T : Any> inlineValue(value: T): T {
val valueName = value::class.primaryConstructor!!.parameters[0].name
val valueProperty = value::class.declaredMemberProperties
.find { it.name == valueName }!! as KProperty1<T, *>
return valueProperty.get(value) as T
}
inline fun <reified T : Any> MockKMatcherScope.anyInlineValue(): T {
val valueConstructor = T::class.primaryConstructor!!
val valueType = valueConstructor.parameters[0].type.classifier as KClass<*>
val callRecorder = getProperty("callRecorder") as CallRecorder
val anyMatcher = callRecorder.matcher(ConstantMatcher<T>(true), valueType)
return valueConstructor.call(anyMatcher)
}
val KClass<*>.isInline: Boolean
get() = !isData &&
primaryConstructor?.parameters?.size == 1 &&
java.declaredMethods.any { it.name == "box-impl" } |
Should work now. |
on mockK 1.12 this still doesn't work (after #633): Please see the following example: import io.mockk.every
import io.mockk.junit5.MockKExtension
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(MockKExtension::class)
internal class a {
@Test
fun t() {
val aService = mockk<AService>()
every { aService.getById(any()) } returns AServiceIdReturn("tralala") // this fails with NPE
assertThat(aService.getById(AServiceId("1"))).usingRecursiveComparison()
.isNotEqualTo(AServiceIdReturn("123"))
.isEqualTo(AServiceIdReturn("tralala"))
}
}
class AService {
fun getById(id: AServiceId): AServiceIdReturn {
return AServiceIdReturn("123")
}
}
@JvmInline
value class AServiceId(val s: String)
@JvmInline
value class AServiceIdReturn(val s: String) And the stack trace is:
|
there is a test in place for the problem ist that the initialization for an value class creates an value class object with a null value field. |
In my case, this works on 1.10.6, and doesn't work with 1.12.0 (kotlin 1.5.21):
I get ClassCastException between SomeData and Long. |
Hi! I have same |
The same here, my workaround is to convert value classes to just classes - this is performance hit because primitives and inline classes are much better handled, but no fix in sight I am afraid. |
Any updates regarding this @Raibaz ? |
I'm on Kotlin 1.7.0 and value classes aren't supported by mockk I updated the workaround from this comment to use the The workarounds are 2 years old though - why aren't they in the main codebase? Have developers moved onto another mocking library? import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible
// Work-around for mocking inline-classes
// https://github.com/mockk/mockk/issues/152#issuecomment-631796323
inline fun <reified T : Any> MockKMatcherScope.anyValue(): T =
if (T::class.isValue) {
anyInline()
} else {
any()
}
inline fun <reified T : Any> MockKMatcherScope.anyInline(): T =
T::class.primaryConstructor!!.run {
this.isAccessible = true
val valueType = parameters[0].type.classifier as KClass<*>
call(match(ConstantMatcher(true), valueType))
}
fun <T : Any> MockKMatcherScope.match(matcher: Matcher<T>, type: KClass<T>): T =
(getProperty("callRecorder") as MockKGateway.CallRecorder).matcher(matcher, type) I would really appreciate it if there was a similar workaround for slots and capture! I tried creating one based on this comment - but it's very confusing. // doesn't work :(
inline fun <reified T : Any> MockKMatcherScope.captureValue(slot: CapturingSlot<T>): T =
if (T::class.isValue) {
captureInline(slot)
} else {
capture(slot)
}
inline fun <reified T : Any> MockKMatcherScope.captureInline(slot: CapturingSlot<T>): T =
T::class.primaryConstructor!!.run {
this.isAccessible = true
val valueType = parameters[0].type.classifier as KClass<*>
val valueMatcher = CapturingSlotMatcher(CapturingSlot(), valueType)
val valueSlot = valueMatcher.captureSlot
call(match(CapturingSlotMatcher(slot, T::class), valueType)
} |
@aSemy I managed to implement a working inline fun <reified T : Any> MockKMatcherScope.anyValue(): T {
if (!T::class.isValue) return any()
val constructor = T::class.primaryConstructor!!.apply { isAccessible = true }
val rawType = constructor.parameters[0].type.classifier as KClass<*>
val anyRawValue = callRecorder.matcher(ConstantMatcher<T>(true), rawType)
return constructor.call(anyRawValue)
}
inline fun <reified T : Any> MockKMatcherScope.captureValue(slot: CapturingSlot<T>): T {
if (!T::class.isValue) return capture(slot)
val constructor = T::class.primaryConstructor!!.apply { isAccessible = true }
val rawType = constructor.parameters[0].type.classifier as KClass<*>
val anyRawValue = callRecorder.matcher(CapturingValueSlotMatcher(slot, constructor, rawType), rawType)
return constructor.call(anyRawValue)
}
val MockKMatcherScope.callRecorder: MockKGateway.CallRecorder
get() = getProperty("callRecorder") as MockKGateway.CallRecorder
data class CapturingValueSlotMatcher<T : Any>(
val captureSlot: CapturingSlot<T>,
val valueConstructor: KFunction<T>,
override val argumentType: KClass<*>,
) : Matcher<T>, CapturingMatcher, TypedMatcher, EquivalentMatcher {
override fun equivalent(): Matcher<Any> = ConstantMatcher(true)
@Suppress("UNCHECKED_CAST")
override fun capture(arg: Any?) {
if (arg == null) {
captureSlot.isNull = true
} else {
captureSlot.isNull = false
captureSlot.captured = valueConstructor.call(arg)
}
captureSlot.isCaptured = true
}
override fun match(arg: T?): Boolean = true
override fun toString(): String = "slotCapture<${argumentType.simpleName}>()"
} |
Great! That looks really tidy. Can we get this committed to the main repo? If you'd like I can take your code and start a PR with it @qoomon? |
I'm about to implement it and create a PR. |
@aSemy I think I need help. I can't get the project working. So feel free to create a PR. BTW here are some tests already. package me.qoomon.enhancements.mockk
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import org.junit.jupiter.api.Test
import strikt.api.expectThat
import strikt.assertions.isEqualTo
class MockkTest {
@Test
fun `any matcher for value class`() {
// given
val mock = mockk<ValueServiceDummy>(relaxed = true)
val givenResult = 1
every { mock.doSomething(anyValue()) } returns givenResult
// when
val result = mock.doSomething(ValueDummy("moin"))
// then
expectThat(result).isEqualTo(givenResult)
}
@Test
fun `slot for value class`() {
// given
val mock = mockk<ValueServiceDummy>(relaxed = true)
val slot = slot<ValueDummy>()
val givenResult = 1
every { mock.doSomething(captureValue(slot)) } returns givenResult
val givenParameter = ValueDummy("s")
// when
val result = mock.doSomething(givenParameter)
// then
expectThat(result).isEqualTo(givenResult)
expectThat(slot.captured).isEqualTo(givenParameter)
}
@Test
fun `value class as return value`() {
// given
val mock = mockk<ValueServiceDummy>(relaxed = true)
val givenResult = ValueDummy("moin")
every { mock.getSomething() } returns givenResult
// when
val result = mock.getSomething()
// then
expectThat(result).isEqualTo(givenResult)
}
}
@JvmInline
value class ValueDummy(val value: String)
interface ValueServiceDummy {
fun doSomething(value: ValueDummy): Int
fun getSomething(): ValueDummy
} |
/**
* POLYFILL for kotlin version < 1.5
* will be shadowed by implementation in kotlin SDK 1.5+
*
* @return true if this is an inline class, else false
*/
private val <T : Any> KClass<T>.isValue: Boolean
get() = !isData &&
primaryConstructor?.parameters?.size == 1 &&
java.declaredMethods.any { it.name == "box-impl" } |
Thank you <3 |
Doesn't seem to work in my case with coEvery :(
Week is declared as @JvmInline
internal value class Week @VisibleForTesting constructor(
val firstDay: LocalDate
) : Comparable<Week> It is passed to suspend function directly as simple parameter. Kotlin version is 1.7.10 |
@aSemy I've created further tests, however I was not able fix it in away so that all test are passing. There are quite some location where value classes need to be handled or already be handled. Maybe you are capable to fix these tests. class MockkTest {
interface ServiceDummy {
fun getAny(): Any
fun getResult(): Result<Int>
fun execute(block: ServiceDummy.() -> Int)
}
@Test
fun `return Result for function with Result return type`() {
val givenValue = Result.success(42)
val service = mockk<ServiceDummy> {
every { getResult() } returns givenValue
}
val result = service.getResult()
assertEquals(givenValue, result)
verify { dummy.getResult() }
}
@Test
fun `return Result for function with Any return type`() {
val givenValue = Result.success(42)
val service = mockk<ServiceDummy> {
every { getAny() } returns givenValue
}
val result = service.getAny()
assertEquals(givenValue, result)
verify { dummy.getAny() }
}
@Test
fun `pass extension blocks as parameter`() {
val givenValue = 42
val dummy = mockk<ServiceDummy> {
every { execute(any()) } answers {
val block = arg<ServiceDummy.() -> Int>(0)
this@mockk.block()
}
}
val result = dummy.execute { givenValue }
assertEquals(givenValue, result)
verify { dummy.execute(any()) }
}
} |
thanks @qoomon. I'll look into this after #855 is done. I suspect something like the fix I proposed here #849 (comment) is needed, to ensure consistent behaviour. I've also been pondering using some code generation (like Kotlin Poet) to generate tests for ALL possible combinations of value class args/responses, nullables, mock types, matcher types... |
That sounds interesting. I would be excited to see your solution. |
Yep that'd be pretty cool! |
Just to mention it: the workarounds mentioned above don't work if the Value-Class has a generic parameter.
=> The method |
Unfortunately, there is still an issue with this feature. If you have an instance that extends from a class with generic values, if the output value of an execution of a function is a value class instance a In this case abstract class Mapper<in T, out R> {
suspend fun map(parameter: T) : R
} In this case value class ProductItemUiModel(value: String) {
// Some value class validation
} private val productToProductInformationUiModelMapper: ProductToProductInformationUiModelMapper = mockk()
private val thingToTest: ThingToTest = ThingToTest(productToProductInformationUiModelMapper)
@Test
fun `test somthing`() {
val expectedValue = getSomeExpectedValue()
val productItemUiModelValue = expectedValue.productItemUiModelValue
val product = generateRandomProduct()
coEvety { productToProductInformationUiModelMapper.map(product) } returns
ProductItemUiModel(productItemUiModelValue)
runTest {
val actualValue = thintToTest.doSomethingCool(product)
}
} Running this test throws:
|
I am trying to use Kotlin's experimental
inline class
feature. I have an interface with a method accepting a parameter with an inline class type. In this case,every
function throws an exception.Prerequisites
My Kotlin version is: 1.3.0-rc-146
Expected Behavior
I should be able to define
every
withany()
in parameters.Current Behavior
Test throws an exception.
Failure Information (for bugs)
See the exception details in Failure logs section.
Steps to Reproduce
This is a test case. I am running this with
Kotlin 1.3.0-rc-146
,kotlin-reflect 1.3.0-rc-146
,JUnit 5.1.0
,mockk 1.8.9.kotlin13
.Context
Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions.
Failure Logs
The text was updated successfully, but these errors were encountered: