Skip to content

Stubbing Kotlin object singletons #3652

@jselbo

Description

@jselbo

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions