My goal is to stub methods on a Kotlin object class.
Example:
object MyKotlinObject {
fun foo(): String { throw RuntimeException("hello") }
}
In the bytecode generated by Kotlin, it is basically equivalent to this Java:
public class MyKotlinObject {
static MyKotlinObject INSTANCE = new MyKotlinObject();
// instance method
public String foo() { ... }
}
Note we already successfully stub methods on Kotlin companion objects using mockConstruction(Foo.Companion.class) + when(Foo.Companion.bar()).thenReturn(...)
(See this issue I raised in mockito-kotlin repo about whether this is the recommended approach: mockito/mockito-kotlin#536 -- but at least this works. )
However, the bytecode for top-level Kotlin objects is a little different because there is no explicit constructor. I did a bit of debugging here, and here is my investigation:
The root of the issue seems to be that - even after class retransformation, MockMethodAdvice.isConstructorMock is never invoked, so the instance is not registered as a mock, and stubbing foo() calls the real code, throwing an exception.
Uninstrumented bytecode for MyKotlinObject contains <clinit> which calls MyKotlinObject.<init> constructor and assigns to static INSTANCE field:
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #2 // class com/example/myapplication/model/MyKotlinObject
3: dup
4: invokespecial #22 // Method "<init>":()V
7: putstatic #25 // Field INSTANCE:Lcom/example/myapplication/model/MyKotlinObject;
However, there is no explicit method definition for the constructor.
Stepping into MockMethodAdvice.ConstructorShortcut wrapper, I confirmed Byte-Buddy classreader reports the method, and we successfully invoke the visit methods to populate the new constructor bytecode. I confirmed the ClassWriter adds the new instructions to the code buffer.
But, the transformed bytecode does not contain the isConstructorMock call in its constructor, in fact it still has no explicit constructor at all. Interestingly the method names are part of the constant pool, just never used:
#68 = Utf8 isConstructorMock
#69 = Utf8 (Ljava/lang/String;Ljava/lang/Class;)Z
#70 = NameAndType #68:#69 // isConstructorMock:(Ljava/lang/String;Ljava/lang/Class;)Z
#71 = Methodref #67.#70 // org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.isConstructorMock:(Ljava/lang/String;Ljava/lang/Class;)Z
At this point I am lost and I'm not sure if this is a Byte-Buddy bug or an integration bug here on the Mockito side.
cc @raphw sorry to tag you directly. Any chance you can bring some insight since you also wrote the code for mockConstruction?
My goal is to stub methods on a Kotlin
objectclass.Example:
In the bytecode generated by Kotlin, it is basically equivalent to this Java:
Note we already successfully stub methods on Kotlin
companion objects usingmockConstruction(Foo.Companion.class)+when(Foo.Companion.bar()).thenReturn(...)(See this issue I raised in mockito-kotlin repo about whether this is the recommended approach: mockito/mockito-kotlin#536 -- but at least this works. )
However, the bytecode for top-level Kotlin
objects is a little different because there is no explicit constructor. I did a bit of debugging here, and here is my investigation:The root of the issue seems to be that - even after class retransformation,
MockMethodAdvice.isConstructorMockis never invoked, so the instance is not registered as a mock, and stubbingfoo()calls the real code, throwing an exception.Uninstrumented bytecode for
MyKotlinObjectcontains<clinit>which callsMyKotlinObject.<init>constructor and assigns to staticINSTANCEfield:However, there is no explicit method definition for the constructor.
Stepping into
MockMethodAdvice.ConstructorShortcutwrapper, I confirmed Byte-Buddy classreader reports the method, and we successfully invoke thevisitmethods to populate the new constructor bytecode. I confirmed the ClassWriter adds the new instructions to thecodebuffer.But, the transformed bytecode does not contain the
isConstructorMockcall in its constructor, in fact it still has no explicit constructor at all. Interestingly the method names are part of the constant pool, just never used:At this point I am lost and I'm not sure if this is a Byte-Buddy bug or an integration bug here on the Mockito side.
cc @raphw sorry to tag you directly. Any chance you can bring some insight since you also wrote the code for
mockConstruction?