mockito-scala icon indicating copy to clipboard operation
mockito-scala copied to clipboard

Mockito won't stub method as expected if the method implemented with Singleton pattern.

Open CharlesZ-Chen opened this issue 2 years ago • 0 comments

I have a java class (but I think java shouldn't matter, the issue should persist even given a scala implementation)

abstract class MyClass {
   String fieldToCacheSerialization = null;
   
   public String serialize() {
      if(fieldToCacheSerialization == null) {
           fieldToCacheSerialization = expensiveSerialization();
      }
      return fieldToCacheSerialization;
   }
  
   /** delegate to sub class implementation, but it is expensive operation
   * and therefore we want to cache it.
   * /
   public abstract String expensiveSerialization() {...}
}

I want to stub MyClass.serialize() call in my scala test suite via Mockito:

test("some test should work as expected") {
   val myClassMock = mock[MyClass]
   when(myClassMock.serialize()).thenReturn("stubbedSerialization") // <--- This will not work as expected.
}

I found the stubbing will not work, as Mockito will register a method stub for MyClass.expensiveSerialization() instead of MyClass.serialize() -- however, during the actual method invocation when Mockito registering the stub, fieldToCacheSerialization will initialize with an empty String (I guess that is the smartNull response for a non-stub yet expensiveSerialization() invocation?). This will lead the stub register on MyClass.expensiveSerialization() NEVER served stub call in test!

I currently have to workaround with:

test("some test should work as expected") {
   val myClassMock = mock[MyClass]
   when(myClassMock.expensiveSerialization()).thenReturn("stubbedSerialization") // <-- Add this line fix the issue
   when(myClassMock.serialize()).thenReturn("stubbedSerialization")

However, I kind of dislike the workaround as:

  1. It leaks MyClass implementation details to my test suite -- in my test suite, MyClass.serialize() is the contact point for my test scope - I do not want to set up the stub only with having to know how MyClass.serialize() is implemented
  2. It is tricky that stubbing only on myClassMock.serialize() won't work -- took me a lot of time to realize is Mockito didn't stub as expected (thought my implementation was wrong and mislead me to debug my implementations).

Wondering is there a way to tell Mockito just directly stub the method invocation that user specified instead of trying to "infer" a leaf method invocation to stub with? (e.g. do not register stub on MyClass.expensiveSerialization() when user is actually specify the stub on MyClass.serialize()).

Thanks a lot!

CharlesZ-Chen avatar Aug 11 '22 19:08 CharlesZ-Chen