Repository: mockito/mockito
Issue: mockito3.4.0 InlineByteBuddyMockMaker can't work with spring cglib proxy, but ByteBuddyMockMaker works
Labels: awaiting response

I upgrade mockito to 3.4.0, and the mockMaker is InlineByteBuddyMockMaker, I use SpyBean to spy a spring bean, and then verify the bean, it throws NotAMockException.
![image](https://user-images.githubusercontent.com/16514353/87801421-4492da80-c882-11ea-8ebc-7f1ee4c80f9c.png)

then i debug, find that the mocks.get(mock) can't find it because the mock is CGLIB proxy, not the original type.

when I use the verify((AppGroupClusterAction) AopTestUtils.getTargetObject(appGroupClusterAction)) to verify. it works.

but the before mockMaker ByteBuddyMockMaker works. so can the InlineByteBuddyMockMaker be compatible with the before？

Issue comments:
In this case, the mock is not registered correctly. Mockito has no knowledge of Spring and could not do much about this. Have you adressed this issue with Spring? How do you get hold of this proxied instance?
``` java
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
@AutoConfigureMockMvc
public class AppGroupClusterActionTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private AppGroupClusterService appGroupClusterService;

  @SpyBean
  private AppGroupClusterAction appGroupClusterAction;

  @Captor
  private ArgumentCaptor<Object[]> argumentCaptor;

  @Test
  public void testUpdateExecuteAuditSpel() throws Exception {
    ServiceResult mockRes = new ServiceResult();
    mockRes.setSuccess(true);
    doReturn(mockRes).when(appGroupClusterService).update(
        Mockito.any(), Mockito.any(), Mockito.any(),
        Mockito.any(), Mockito.any(), Mockito.any(),
        Mockito.any(), Mockito.any(), Mockito.any());

    mockMvc.perform(
        MockMvcRequestBuilders.post("/platforms/{platform}/appGroupNames/{appGroupName}/clusterNames/{clusterName}/update",
            "platform", "appGroupName", "clusterName").content("{}"))
        .andExpect(status().isOk())
        .andReturn();

//    not work
//    verify(appGroupClusterAction).addAfterAuditResult(argumentCaptor.capture());
    verify((AppGroupClusterAction) AopTestUtils.getTargetObject(appGroupClusterAction)).addAfterAuditResult(argumentCaptor.capture());
    assertArrayEquals(argumentCaptor.getValue(), new Object[]{"appGroupName", "platform", "clusterName", "{}"});

    verify((AppGroupClusterAction) AopTestUtils.getTargetObject(appGroupClusterAction)).addAfterAuditResult(argumentCaptor.capture());
    assertArrayEquals(argumentCaptor.getValue(), new Object[]{"appGroupName", "platform", "clusterName", "{}"});
  }
}
```
this is the code.  I haven't  addressed this issue with Spring, because it works before mockito3.4.0, I don't know whether the mockito need to be compatible with before？
I'd say this issue would need to be adressed in Spring. I assume that the `MockBean` does not actually get the object injected that Mockito issues.
I've [taken a look at this from the Spring Boot side of things](https://github.com/spring-projects/spring-boot/issues/22416#issuecomment-670464669). The TL;DR is that Spring's creating a CGLib proxy of the spy that Mockito has created. This proxied spy is passed into `when` and `SubclassByteBuddyMockMaker` correctly identifies that it's a mock while `InlineByteBuddyMockMaker` does not. You can work around the problem by passing the target of Spring's proxy (the Mockito spy) into `when`, but my expectation is that the two mock makers should have the same ability to identify a mock and both should cope with the spy having been proxied.

> I haven't addressed this issue with Spring, because it works before mockito3.4.0

I should also note that, in my testing, this isn't the case. The problem exists with Mockito 3.4, 3.3, and 3.1 with the difference in behaviour depending on whether or not you're using mockito-inline.
@raphw @mockitoguy  can you help?
Thanks for filing the issue. Could you please provide us a minimal reproduction case that we can clone/download to debug the issue? Thanks in advance!
Sorry, I missed this. 

The reason this works with the `SubclassByteBuddyMockMaker` is that Spring proxies the (internal) `MockAccess` interface which we implement in the mock to extract the mock state which is stored in a field of the mock instance. We could just as much had read that field directly to avoid the interface; and this wouldn't even be such a bad idea as it less pollutes the reflection API. But at this time, when we call our internal interface method, it gets redirected to the actual proxy by Spring which is why this functions (accidentally).

The reason it does not work with the `InlineByteBuddyMockMaker` is that we cannot implement interfaces in a class after it has been loaded. The whole premise of the inline mock maker is to keep changes invisible to make use of the retransformation facilities even after class loading. Therefore, we need to identify mocks by the object identity which changes once Spring changes the object. We cannot change this, from our perspective nothing is broken.

From Mockito's side things are simple: if you do not provide "our" mock objects to us, we do not guarantee to work, not in the past or in the future. That it does currently work with the subclass mock maker facility is rather accidental and might change in the future. If Spring cannot do anything about this, you'd need to unproxy the objects explicitly.
> If Spring cannot do anything about this

As things stand, I don't believe we can as there's no opportunity for Spring to remove its proxy when the proxied spy is passed into `given` or `when`.

I've [wondered in the past](https://github.com/spring-projects/spring-boot/issues/22281#issuecomment-665762299) if Mockito could offer a low-level API that provides a mechanism for pre-processing the instance that's passed in, but concluded that it wouldn't be broadly usable. I'd be delighted if someone who's more familiar with Mockito's internals believes it could be done.

In the absence of some inspiration on the Mockito side, our [current plan](https://github.com/spring-projects/spring-boot/issues/22742) is to make it easier for users to work with both the Spring proxy and the "bare" Mockito spy.


I could certainly add a mechanism for this, it would not create too much overhead and we already have a plugin mechanism in place for it. We would just add a non-operational mechanism by default but allow frameworks to unwrap "their" mocks. You could declare this by Mockito's default plugin mechanism.

I will look into adding something like this to Mockito.
@wilkinsona

I just added an API. Could you try the *mock-resolver-plugin* branch where Spring would need to implement the `MockResolver` interface as a public class with a default constructor and add a file *org.mockito.plugins.MockResolver* to the *mockito-extensions* folder with the fully qualified class name. The resolver needs to check if a mock instance can be unwrapped and return this instance if applicable.

This way, I think this problem can be solved and Spring stops relying on the implementation detail. 
Thanks, @raphw. I think this moves things in the right direction and allows almost all of our Mockito-related tests to pass with the inline mock maker.

I am, however, still seeing a failure when calling `Mockito.reset(T)` with a spy that has been proxied with CGLib. The `MockHandler` is retrieved successfully (as our resolver removes the CGLib proxy) but it then fails in `InlineByteBuddyMockMaker.resetMock(Object, MockHandler, MockCreationSettings)` as the mock that is passed in is still CGLib-proxied. I wonder if reset should resolve the mock and use that both for getting the handler and for calling the mock maker? Something like this:

```java
public static <T> void resetMock(T mock) {
    mock = resolve(mock);
    MockHandler oldHandler = getMockHandler(mock);
    MockCreationSettings settings = oldHandler.getMockSettings();
    MockHandler newHandler = createMockHandler(settings);

    mockMaker.resetMock(mock, newHandler, settings);
}
```
Good catch! I'll fix that up! 
I updated the branch and doublechecked that I did not miss anything else. Let me know if this works for you!
@wilkinsona Did you have a chance to validate my latest revision of this?
Not yet, sorry. I'll try and take another look at it this week.
It took a little longer than I'd hoped, but I've just tried the updated branch and all of Spring Boot's Mockito-related tests now pass with the inline mock maker. Thank you.
@raphw @wilkinsona  Can someone tell me how I can leverage or otherwise take advantage of whatever fix was made in #2042 and get my @SpyBeans to work correctly with Mockito of any version? I've tried changing my version from v3.4.0 to v3.6.0 (where the change was ultimately merged) but that alone has not improved the situation. Thanks in advance!

Pull request review comments:
Nit: extract this out in a private method and use that below as well.
Can we add a test that uses multiple resolvers? I can think of the following scenarios:

1. Multiple resolvers, no match
2. Multiple resolvers, one match
3. Multiple resolvers, multiple match

For scenario 3, should we throw maybe? What if it is order-dependent? Should we continue iterating over the resolvers until none match?
I think multiple resolvers should be allowed. You can technically add several in your specified order and decompose multiple levels of nesting. It's of course an obscure scenario, but it makes sense to not forbid it since it would be extra work. I can add the tests you suggest.
Fully agree, I hope for some feedback first, too.
Hmm, registering multiple converters in a test is not quite that easy since it would require multiple modules. I don't think its worth the noise to add this functionality just for testing such a simple corner case.
Understood. How would that work when a user wants to integrate with multiple frameworks that all expose a resolver?
They would all expose their resolver and they would be invoked in their registration order. For instance, let's say Hibernate offered a Mockito-based mocking API but returns Hibernate proxies on mocks. Those proxies are by definition never Spring Beans. But if one passed a Spring bean or a Hibernate proxy, both would traverse the resolver chain and be unpacked accordingly.

I assume that normally, only one resolver would apply to any wrapped mock but since Mockito should remain agnostic to this, that would be the best approach in my eyes.
Why were the generic parameters removed from this and the other methods? I understand that they are not used, but I wonder if there are unintended side-effects of this change.
Just as a cleanup on the side since I am touching the code already. The generic type is erased to `Object` and has no other function, that's why I removed it. No impact on source or binary compatibility.
This needs to be marked as `@Incubating`
Arghs, I missed this and just hit merge. Sorry, we can still add this, though.
Yes let's still add it.

Patch that resolved the issue:
diff --git a/src/main/java/org/mockito/internal/configuration/plugins/PluginFinder.java b/src/main/java/org/mockito/internal/configuration/plugins/PluginFinder.java
index 7385afb184..3ca2ac5cd4 100644
--- a/src/main/java/org/mockito/internal/configuration/plugins/PluginFinder.java
+++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginFinder.java
@@ -6,6 +6,8 @@
 
 import java.io.InputStream;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
 
 import org.mockito.exceptions.base.MockitoException;
 import org.mockito.internal.util.io.IOUtil;
@@ -43,4 +45,30 @@ String findPluginClass(Iterable<URL> resources) {
         }
         return null;
     }
+
+    List<String> findPluginClasses(Iterable<URL> resources) {
+        List<String> pluginClassNames = new ArrayList<>();
+        for (URL resource : resources) {
+            InputStream s = null;
+            try {
+                s = resource.openStream();
+                String pluginClassName = new PluginFileReader().readPluginClass(s);
+                if (pluginClassName == null) {
+                    // For backwards compatibility
+                    // If the resource does not have plugin class name we're ignoring it
+                    continue;
+                }
+                if (!pluginSwitch.isEnabled(pluginClassName)) {
+                    continue;
+                }
+                pluginClassNames.add(pluginClassName);
+            } catch (Exception e) {
+                throw new MockitoException(
+                        "Problems reading plugin implementation from: " + resource, e);
+            } finally {
+                IOUtil.closeQuietly(s);
+            }
+        }
+        return pluginClassNames;
+    }
 }
diff --git a/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java b/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java
index 8f8f76edcb..296397edc3 100644
--- a/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java
+++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java
@@ -6,7 +6,9 @@
 
 import java.io.IOException;
 import java.net.URL;
+import java.util.ArrayList;
 import java.util.Enumeration;
+import java.util.List;
 
 import org.mockito.internal.util.collections.Iterables;
 import org.mockito.plugins.PluginSwitch;
@@ -56,4 +58,36 @@ public <T> T loadImpl(Class<T> service) {
                     "Failed to load " + service + " implementation declared in " + resources, e);
         }
     }
+
+    public <T> List<T> loadImpls(Class<T> service) {
+        ClassLoader loader = Thread.currentThread().getContextClassLoader();
+        if (loader == null) {
+            loader = ClassLoader.getSystemClassLoader();
+        }
+        Enumeration<URL> resources;
+        try {
+            resources = loader.getResources("mockito-extensions/" + service.getName());
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to load " + service, e);
+        }
+
+        try {
+            List<String> classesOrAliases =
+                    new PluginFinder(pluginSwitch)
+                            .findPluginClasses(Iterables.toIterable(resources));
+            List<T> impls = new ArrayList<>();
+            for (String classOrAlias : classesOrAliases) {
+                if (classOrAlias.equals(alias)) {
+                    classOrAlias = plugins.getDefaultPluginClass(alias);
+                }
+                Class<?> pluginClass = loader.loadClass(classOrAlias);
+                Object plugin = pluginClass.getDeclaredConstructor().newInstance();
+                impls.add(service.cast(plugin));
+            }
+            return impls;
+        } catch (Exception e) {
+            throw new IllegalStateException(
+                    "Failed to load " + service + " implementation declared in " + resources, e);
+        }
+    }
 }
diff --git a/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java b/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java
index 3d724aa49a..e76bb9729f 100644
--- a/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java
+++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java
@@ -7,6 +7,8 @@
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
+import java.util.Collections;
+import java.util.List;
 
 import org.mockito.plugins.PluginSwitch;
 
@@ -90,4 +92,32 @@ public Object invoke(Object proxy, Method method, Object[] args)
                     });
         }
     }
+
+    /**
+     * Scans the classpath for given {@code pluginType} and returns a list of its instances.
+     *
+     * @return An list of {@code pluginType} or an empty list if none was found.
+     */
+    @SuppressWarnings("unchecked")
+    <T> List<T> loadPlugins(final Class<T> pluginType) {
+        try {
+            return initializer.loadImpls(pluginType);
+        } catch (final Throwable t) {
+            return Collections.singletonList(
+                    (T)
+                            Proxy.newProxyInstance(
+                                    pluginType.getClassLoader(),
+                                    new Class<?>[] {pluginType},
+                                    new InvocationHandler() {
+                                        @Override
+                                        public Object invoke(
+                                                Object proxy, Method method, Object[] args)
+                                                throws Throwable {
+                                            throw new IllegalStateException(
+                                                    "Could not initialize plugin: " + pluginType,
+                                                    t);
+                                        }
+                                    }));
+        }
+    }
 }
diff --git a/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java b/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java
index ba7aae1728..9a12d1755a 100644
--- a/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java
+++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java
@@ -7,6 +7,8 @@
 import org.mockito.internal.creation.instance.InstantiatorProviderAdapter;
 import org.mockito.plugins.*;
 
+import java.util.List;
+
 class PluginRegistry {
 
     private final PluginSwitch pluginSwitch =
@@ -31,6 +33,9 @@ class PluginRegistry {
     private final MockitoLogger mockitoLogger =
             new PluginLoader(pluginSwitch).loadPlugin(MockitoLogger.class);
 
+    private final List<MockResolver> mockResolvers =
+            new PluginLoader(pluginSwitch).loadPlugins(MockResolver.class);
+
     PluginRegistry() {
         Object impl =
                 new PluginLoader(pluginSwitch)
@@ -100,4 +105,13 @@ AnnotationEngine getAnnotationEngine() {
     MockitoLogger getMockitoLogger() {
         return mockitoLogger;
     }
+
+    /**
+     * Returns a list of available mock resolvers if any.
+     *
+     * @return A list of available mock resolvers or an empty list if none are registered.
+     */
+    List<MockResolver> getMockResolvers() {
+        return mockResolvers;
+    }
 }
diff --git a/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java b/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java
index 603a03008a..da225a2886 100644
--- a/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java
+++ b/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java
@@ -6,6 +6,8 @@
 
 import org.mockito.plugins.*;
 
+import java.util.List;
+
 /**
  * Access to Mockito behavior that can be reconfigured by plugins
  */
@@ -71,6 +73,15 @@ public static MockitoLogger getMockitoLogger() {
         return registry.getMockitoLogger();
     }
 
+    /**
+     * Returns a list of available mock resolvers if any.
+     *
+     * @return A list of available mock resolvers or an empty list if none are registered.
+     */
+    public static List<MockResolver> getMockResolvers() {
+        return registry.getMockResolvers();
+    }
+
     /**
      * @return instance of mockito plugins type
      */
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java
index 69a7ed21a6..9323368a14 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java
@@ -397,13 +397,17 @@ public MethodVisitor wrap(
                                 .getDeclaredMethods()
                                 .filter(isConstructor().and(not(isPrivate())));
                 int arguments = Integer.MAX_VALUE;
-                boolean visible = false;
+                boolean packagePrivate = true;
                 MethodDescription.InDefinedShape current = null;
                 for (MethodDescription.InDefinedShape constructor : constructors) {
+                    // We are choosing the shortest constructor with regards to arguments.
+                    // Yet, we prefer a non-package-private constructor since they require
+                    // the super class to be on the same class loader.
                     if (constructor.getParameters().size() < arguments
-                            && (!visible || constructor.isPackagePrivate())) {
+                            && (packagePrivate || !constructor.isPackagePrivate())) {
+                        arguments = constructor.getParameters().size();
+                        packagePrivate = constructor.isPackagePrivate();
                         current = constructor;
-                        visible = constructor.isPackagePrivate();
                     }
                 }
                 if (current != null) {
diff --git a/src/main/java/org/mockito/internal/util/DefaultMockingDetails.java b/src/main/java/org/mockito/internal/util/DefaultMockingDetails.java
index e9887a57b5..1ad5a757f8 100644
--- a/src/main/java/org/mockito/internal/util/DefaultMockingDetails.java
+++ b/src/main/java/org/mockito/internal/util/DefaultMockingDetails.java
@@ -73,7 +73,7 @@ public Object getMock() {
         return toInspect;
     }
 
-    private MockHandler<Object> mockHandler() {
+    private MockHandler<?> mockHandler() {
         assertGoodMock();
         return MockUtil.getMockHandler(toInspect);
     }
diff --git a/src/main/java/org/mockito/internal/util/MockUtil.java b/src/main/java/org/mockito/internal/util/MockUtil.java
index 46e782cba0..25a29d2c0d 100644
--- a/src/main/java/org/mockito/internal/util/MockUtil.java
+++ b/src/main/java/org/mockito/internal/util/MockUtil.java
@@ -16,6 +16,7 @@
 import org.mockito.mock.MockName;
 import org.mockito.plugins.MockMaker;
 import org.mockito.plugins.MockMaker.TypeMockability;
+import org.mockito.plugins.MockResolver;
 
 import java.util.function.Function;
 
@@ -55,21 +56,25 @@ public static <T> T createMock(MockCreationSettings<T> settings) {
         return mock;
     }
 
-    public static <T> void resetMock(T mock) {
+    public static void resetMock(Object mock) {
         MockHandler oldHandler = getMockHandler(mock);
         MockCreationSettings settings = oldHandler.getMockSettings();
         MockHandler newHandler = createMockHandler(settings);
 
+        mock = resolve(mock);
         mockMaker.resetMock(mock, newHandler, settings);
     }
 
-    public static <T> MockHandler<T> getMockHandler(T mock) {
+    public static MockHandler<?> getMockHandler(Object mock) {
         if (mock == null) {
             throw new NotAMockException("Argument should be a mock, but is null!");
         }
 
-        if (isMock(mock)) {
-            return mockMaker.getHandler(mock);
+        mock = resolve(mock);
+
+        MockHandler handler = mockMaker.getHandler(mock);
+        if (handler != null) {
+            return handler;
         } else {
             throw new NotAMockException("Argument should be a mock, but is: " + mock.getClass());
         }
@@ -96,7 +101,23 @@ public static boolean isMock(Object mock) {
         // Potentially we could also move other methods to MockitoMock, some other candidates:
         // getInvocationContainer, isSpy, etc.
         // This also allows us to reuse our public API MockingDetails
-        return mock != null && mockMaker.getHandler(mock) != null;
+        if (mock == null) {
+            return false;
+        }
+
+        mock = resolve(mock);
+
+        return mockMaker.getHandler(mock) != null;
+    }
+
+    private static Object resolve(Object mock) {
+        if (mock instanceof Class<?>) { // static mocks are resolved by definition
+            return mock;
+        }
+        for (MockResolver mockResolver : Plugins.getMockResolvers()) {
+            mock = mockResolver.resolve(mock);
+        }
+        return mock;
     }
 
     public static MockName getMockName(Object mock) {
diff --git a/src/main/java/org/mockito/plugins/MockMaker.java b/src/main/java/org/mockito/plugins/MockMaker.java
index fbdaf1d0e6..fcebbb3ae7 100644
--- a/src/main/java/org/mockito/plugins/MockMaker.java
+++ b/src/main/java/org/mockito/plugins/MockMaker.java
@@ -85,7 +85,7 @@ public interface MockMaker {
      *                {@link #getHandler(Object)} will return this instance.
      * @param instance  The object to spy upon.
      * @param <T> Type of the mock to return, actually the <code>settings.getTypeToMock</code>.
-     * @return
+     * @return The spy instance, if this mock maker supports direct spy creation.
      * @since 3.5.0
      */
     default <T> Optional<T> createSpy(
diff --git a/src/main/java/org/mockito/plugins/MockResolver.java b/src/main/java/org/mockito/plugins/MockResolver.java
new file mode 100644
index 0000000000..6eaa756f7e
--- /dev/null
+++ b/src/main/java/org/mockito/plugins/MockResolver.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2020 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito.plugins;
+
+/**
+ * A mock resolver offers an opportunity to resolve a mock from any instance that is
+ * provided to the {@link org.mockito.Mockito}-DSL. This mechanism can be used by
+ * frameworks that provide mocks that are implemented by Mockito but which are wrapped
+ * by other instances to enhance the proxy further.
+ */
+public interface MockResolver {
+
+    /**
+     * Returns the provided instance or the unwrapped mock that the provided
+     * instance represents. This method must not return {@code null}.
+     * @param instance The instance passed to the {@link org.mockito.Mockito}-DSL.
+     * @return The provided instance or the unwrapped mock.
+     */
+    Object resolve(Object instance);
+}


Test patch:
diff --git a/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MockResolverTest.java b/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MockResolverTest.java
new file mode 100644
index 0000000000..bd51fe6dc2
--- /dev/null
+++ b/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MockResolverTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2020 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockitousage.plugins.resolver;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+@MockitoSettings(strictness = Strictness.WARN)
+@ExtendWith(MockitoExtension.class)
+class MockResolverTest {
+
+    @Test
+    void mock_resolver_can_unwrap_mocked_instance() {
+        Foo mock = mock(Foo.class), wrapper = new MockWrapper(mock);
+        when(wrapper.doIt()).thenReturn(123);
+        assertThat(mock.doIt()).isEqualTo(123);
+        assertThat(wrapper.doIt()).isEqualTo(123);
+        verify(wrapper, times(2)).doIt();
+    }
+
+    interface Foo {
+        int doIt();
+    }
+
+    static class MockWrapper implements Foo {
+
+        private final Foo mock;
+
+        MockWrapper(Foo mock) {
+            this.mock = mock;
+        }
+
+        Object getMock() {
+            return mock;
+        }
+
+        @Override
+        public int doIt() {
+            return mock.doIt();
+        }
+    }
+
+}
diff --git a/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MyMockResolver.java b/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MyMockResolver.java
new file mode 100644
index 0000000000..e14804f6c5
--- /dev/null
+++ b/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MyMockResolver.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2020 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockitousage.plugins.resolver;
+
+import org.mockito.plugins.MockResolver;
+
+public class MyMockResolver implements MockResolver {
+
+    @Override
+    public Object resolve(Object instance) {
+        if (instance instanceof MockResolverTest.MockWrapper) {
+            return ((MockResolverTest.MockWrapper) instance).getMock();
+        }
+        return instance;
+    }
+}
diff --git a/subprojects/extTest/src/test/java/org/mockitousage/plugins/switcher/PluginSwitchTest.java b/subprojects/extTest/src/test/java/org/mockitousage/plugins/switcher/PluginSwitchTest.java
index d61f150bae..000c2e8ca9 100644
--- a/subprojects/extTest/src/test/java/org/mockitousage/plugins/switcher/PluginSwitchTest.java
+++ b/subprojects/extTest/src/test/java/org/mockitousage/plugins/switcher/PluginSwitchTest.java
@@ -7,6 +7,7 @@
 import org.junit.Test;
 import org.mockitousage.plugins.instantiator.MyInstantiatorProvider2;
 import org.mockitousage.plugins.logger.MyMockitoLogger;
+import org.mockitousage.plugins.resolver.MyMockResolver;
 import org.mockitousage.plugins.stacktrace.MyStackTraceCleanerProvider;
 
 import java.util.List;
@@ -25,6 +26,7 @@ public void plugin_switcher_is_used() {
         assertEquals(MyPluginSwitch.invokedFor, asList(MyMockMaker.class.getName(),
             MyStackTraceCleanerProvider.class.getName(),
             MyMockitoLogger.class.getName(),
+            MyMockResolver.class.getName(),
             MyInstantiatorProvider2.class.getName()));
     }
 
diff --git a/subprojects/extTest/src/test/resources/mockito-extensions/org.mockito.plugins.MockResolver b/subprojects/extTest/src/test/resources/mockito-extensions/org.mockito.plugins.MockResolver
new file mode 100644
index 0000000000..7f2835f93a
--- /dev/null
+++ b/subprojects/extTest/src/test/resources/mockito-extensions/org.mockito.plugins.MockResolver
@@ -0,0 +1,1 @@
+org.mockitousage.plugins.resolver.MyMockResolver