mockito / mockito

Most popular Mocking framework for unit tests written in Java
http://mockito.org
MIT License
14.88k stars 2.56k forks source link

Mockito fails mocking when using method reference in class but not when using equivalent lambda syntax #3045

Open 1dEraNCeSIv0 opened 1 year ago

1dEraNCeSIv0 commented 1 year ago

Checklist:

Issue description

When mocking a class using generics in a particular way Mockito will throw an exception when attempting to mock the class. This only occurs if the class under test uses method reference to refer to a private method; calling the same method using regular lambda syntax will allow the creation of the mock

Versions used

Mockito: 5.3.1 Java: 19 OS: Windows 10

ByteBuddy: 1.14.4 (transitive dependency via Mockito 5.3.1, possibly relevant)

(See also the example project down below, based on a recent spring starter from start.spring.io)

How to reproduce using the appended demo project

  1. Import the minimal example project as gradle project into your IDE
  2. Run all tests

Expected

What actually happens

The test LambdaVariantTest.shouldNeverFail works as expected, but MethodReferenceVariantTest.shouldNeverFail will fail with the following stacktrace

org.mockito.exceptions.base.MockitoException: 
Mockito cannot mock this class: class com.example.demo.MethodReferenceVariant.

If you're not sure why you're getting this error, please open an issue on GitHub.

Java               : 19
JVM vendor name    : Eclipse Adoptium
JVM vendor version : 19.0.2+7
JVM name           : OpenJDK 64-Bit Server VM
JVM version        : 19.0.2+7
JVM info           : mixed mode, sharing
OS name            : Windows 10
OS version         : 10.0

You are seeing this disclaimer because Mockito is configured to create inlined mocks.
You can learn about inline mocks and their limitations under item #39 of the Mockito class javadoc.

Underlying exception : org.mockito.exceptions.base.MockitoException: Could not modify all classes [class java.lang.Object, class com.example.demo.MethodReferenceVariant]
    at com.example.demo.MethodReferenceVariantTest.<init>(MethodReferenceVariantTest.java:10)
    at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:67)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:484)
    at org.junit.platform.commons.util.ReflectionUtils.newInstance(ReflectionUtils.java:552)
    at org.junit.jupiter.engine.execution.ConstructorInvocation.proceed(ConstructorInvocation.java:56)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.api.extension.InvocationInterceptor.interceptTestClassConstructor(InvocationInterceptor.java:73)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:62)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestClassConstructor(ClassBasedTestDescriptor.java:363)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateTestClass(ClassBasedTestDescriptor.java:310)
    at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.instantiateTestClass(ClassTestDescriptor.java:79)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:286)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$4(ClassBasedTestDescriptor.java:278)
    at java.base/java.util.Optional.orElseGet(Optional.java:364)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$5(ClassBasedTestDescriptor.java:277)
    at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:105)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:104)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:68)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$2(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:90)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:95)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:91)
    at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:60)
    at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:98)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: org.mockito.exceptions.base.MockitoException: Could not modify all classes [class java.lang.Object, class com.example.demo.MethodReferenceVariant]
    at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:168)
    at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:399)
    at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:190)
    at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:410)
    ... 70 more
Caused by: java.lang.IllegalStateException: 
Byte Buddy could not instrument all classes within the mock's type hierarchy

This problem should never occur for javac-compiled classes. This problem has been observed for classes that are:
 - Compiled by older versions of scalac
 - Classes that are part of the Android distribution
    at org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.triggerRetransformation(InlineBytecodeGenerator.java:285)
    at org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.mockClass(InlineBytecodeGenerator.java:218)
    at org.mockito.internal.creation.bytebuddy.TypeCachingBytecodeGenerator.lambda$mockClass$0(TypeCachingBytecodeGenerator.java:47)
    at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:168)
    at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:399)
    at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:190)
    at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:410)
    at org.mockito.internal.creation.bytebuddy.TypeCachingBytecodeGenerator.mockClass(TypeCachingBytecodeGenerator.java:40)
    at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.createMockType(InlineDelegateByteBuddyMockMaker.java:396)
    at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.doCreateMock(InlineDelegateByteBuddyMockMaker.java:355)
    at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.createMock(InlineDelegateByteBuddyMockMaker.java:334)
    at org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker.createMock(InlineByteBuddyMockMaker.java:56)
    at org.mockito.internal.util.MockUtil.createMock(MockUtil.java:99)
    at org.mockito.internal.MockitoCore.mock(MockitoCore.java:88)
    at org.mockito.Mockito.mock(Mockito.java:2101)
    at org.mockito.Mockito.mock(Mockito.java:2016)
    ... 70 more
Caused by: java.lang.IllegalStateException: size = 2
    at net.bytebuddy.matcher.FilterableList$AbstractBase.getOnly(FilterableList.java:139)
    at net.bytebuddy.description.TypeVariableSource$AbstractBase.findVariable(TypeVariableSource.java:164)
    at net.bytebuddy.description.TypeVariableSource$AbstractBase.findExpectedVariable(TypeVariableSource.java:172)
    at net.bytebuddy.description.type.TypeDescription$Generic$Visitor$Substitutor$ForAttachment.onTypeVariable(TypeDescription.java:2062)
    at net.bytebuddy.description.type.TypeDescription$Generic$Visitor$Substitutor$ForAttachment.onTypeVariable(TypeDescription.java:1973)
    at net.bytebuddy.description.type.TypeDescription$Generic$OfTypeVariable$Symbolic.accept(TypeDescription.java:5931)
    at net.bytebuddy.description.method.ParameterDescription$Latent.getType(ParameterDescription.java:806)
    at net.bytebuddy.description.method.ParameterList$AbstractBase.asTypeList(ParameterList.java:107)
    at net.bytebuddy.description.method.MethodDescription$AbstractBase.hashCode(MethodDescription.java:938)
    at java.base/java.util.HashMap.hash(HashMap.java:338)
    at java.base/java.util.HashMap.put(HashMap.java:618)
    at java.base/java.util.HashSet.add(HashSet.java:229)
    at java.base/java.util.AbstractCollection.addAll(AbstractCollection.java:338)
    at java.base/java.util.HashSet.<init>(HashSet.java:121)
    at net.bytebuddy.dynamic.scaffold.MethodRegistry$Default.prepare(MethodRegistry.java:457)
    at net.bytebuddy.dynamic.scaffold.inline.RedefinitionDynamicTypeBuilder.toTypeWriter(RedefinitionDynamicTypeBuilder.java:203)
    at net.bytebuddy.dynamic.scaffold.inline.AbstractInliningDynamicTypeBuilder.toTypeWriter(AbstractInliningDynamicTypeBuilder.java:122)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase$UsingTypeWriter.make(DynamicType.java:4050)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:3734)
    at org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.transform(InlineBytecodeGenerator.java:402)
    at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:244)
    at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
    at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:541)
    at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
    at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:169)
    at org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.triggerRetransformation(InlineBytecodeGenerator.java:281)
    ... 85 more

Further information

If wanting to reproduce without the provided demo project or importing fails somehow the following is the most relevant code:

// Class not mockable
public class MethodReferenceVariant {

    public <T extends HasCode & HasName> List<T> sortedChoices(List<T> entries) {
        return entries.stream()
            .sorted(this::compareCodes)
            .toList();
    }

    private <T extends HasCode> int compareCodes(T left, T right) {
        return left.getCode().compareTo(right.getCode());
    }

}

// Class mockable
public class LambdaVariant {

    public <T extends HasCode & HasName> List<T> sortedChoices(List<T> entries) {
        return entries.stream()
            .sorted((left, right) -> compareCodes(left, right))
            .toList();
    }

    private <T extends HasCode> int compareCodes(T left, T right) {
        return left.getCode().compareTo(right.getCode());
    }

}

// Interfaces
public interface HasCode {
    Integer getCode();
}

public interface HasName {
    String getName();
}

// Test classes
class MethodReferenceVariantTest {

    private MethodReferenceVariant mock = mock(MethodReferenceVariant.class);

    @Test
    void shouldNeverFail() {
        assertThat(true).isTrue();
    }

}

class LambdaVariantTest {

    private LambdaVariant mock = mock(LambdaVariant.class);

    @Test
    void shouldNeverFail() {
        assertThat(true).isTrue();
    }

}

These 6 classes / interfaces are sufficient to reproduce the issue for me but if more info is required I'm happy to provide it.

JixiuL commented 1 year ago

Hi, I am a university student and new to contributing. I would love to work on this. If possible could I just get some tips on where to start with this issue? Can you assign this to me?

1dEraNCeSIv0 commented 1 year ago

I'm not part of the mockito team but if I were to try and fix this myself I'd:

  1. ensure I could reproduce it on my machine
  2. identify the root cause via debugging
  3. create a fix that solves the issue
  4. open a pull request, describing the error it solves and ask for it to be reviewed and eventually merged

Alternative to 3 and 4: If the issue ends up being with byte buddy or some other library used by mockito then I'd instead open a ticket with them (or possibly fix it in their code and create a pull request with them).

Best of luck :)