diff --git a/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java b/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java index d06a8fb620..559a90d592 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java @@ -21,6 +21,7 @@ class DefaultMockitoPlugins implements MockitoPlugins { private static final Map DEFAULT_PLUGINS = new HashMap<>(); static final String INLINE_ALIAS = "mock-maker-inline"; + static final String PROXY_ALIAS = "mock-maker-proxy"; static final String MODULE_ALIAS = "member-accessor-module"; static { @@ -40,6 +41,7 @@ class DefaultMockitoPlugins implements MockitoPlugins { "org.mockito.internal.configuration.InjectingAnnotationEngine"); DEFAULT_PLUGINS.put( INLINE_ALIAS, "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker"); + DEFAULT_PLUGINS.put(PROXY_ALIAS, "org.mockito.internal.creation.proxy.ProxyMockMaker"); DEFAULT_PLUGINS.put( MockitoLogger.class.getName(), "org.mockito.internal.util.ConsoleMockitoLogger"); DEFAULT_PLUGINS.put( diff --git a/src/main/java/org/mockito/internal/creation/proxy/ProxyMockMaker.java b/src/main/java/org/mockito/internal/creation/proxy/ProxyMockMaker.java new file mode 100644 index 0000000000..31a13124c4 --- /dev/null +++ b/src/main/java/org/mockito/internal/creation/proxy/ProxyMockMaker.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2021 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.creation.proxy; + +import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.debugging.LocationImpl; +import org.mockito.internal.invocation.RealMethod; +import org.mockito.internal.invocation.SerializableMethod; +import org.mockito.internal.util.Platform; +import org.mockito.invocation.MockHandler; +import org.mockito.mock.MockCreationSettings; +import org.mockito.plugins.MockMaker; + +import java.lang.reflect.*; +import java.util.concurrent.atomic.AtomicReference; + +import static org.mockito.internal.invocation.DefaultInvocationFactory.createInvocation; +import static org.mockito.internal.util.StringUtil.join; + +/** + * A mock maker that is using the {@link Proxy} utility and is therefore only capable of mocking interfaces but + * does not rely on manual byte code generation but only uses official and public Java API. + */ +public class ProxyMockMaker implements MockMaker { + + private final Method invokeDefault; + + public ProxyMockMaker() { + Method m; + try { + m = + InvocationHandler.class.getMethod( + "invokeDefault", Object.class, Method.class, Object[].class); + } catch (NoSuchMethodException ignored) { + m = null; + } + invokeDefault = m; + } + + @Override + @SuppressWarnings("unchecked") + public T createMock(MockCreationSettings settings, MockHandler handler) { + Class[] ifaces = new Class[settings.getExtraInterfaces().size() + 1]; + ifaces[0] = settings.getTypeToMock(); + int index = 1; + for (Class iface : settings.getExtraInterfaces()) { + ifaces[index++] = iface; + } + return (T) + Proxy.newProxyInstance( + settings.getTypeToMock().getClassLoader(), + ifaces, + new MockInvocationHandler(handler, settings)); + } + + @Override + public MockHandler getHandler(Object mock) { + if (!Proxy.isProxyClass(mock.getClass())) { + return null; + } + InvocationHandler handler = Proxy.getInvocationHandler(mock); + if (!(handler instanceof MockInvocationHandler)) { + return null; + } + return ((MockInvocationHandler) handler).handler.get(); + } + + @Override + public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) { + ((MockInvocationHandler) Proxy.getInvocationHandler(mock)).handler.set(newHandler); + } + + @Override + public TypeMockability isTypeMockable(Class type) { + return new TypeMockability() { + @Override + public boolean mockable() { + return type.isInterface(); + } + + @Override + public String nonMockableReason() { + return mockable() ? "" : "non-interface"; + } + }; + } + + private class MockInvocationHandler implements InvocationHandler { + + private final AtomicReference> handler; + + private final MockCreationSettings settings; + + private MockInvocationHandler(MockHandler handler, MockCreationSettings settings) { + this.handler = new AtomicReference<>(handler); + this.settings = settings; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getDeclaringClass() == Object.class) { + switch (method.getName()) { + case "hashCode": + return System.identityHashCode(proxy); + case "equals": + return proxy == args[0]; + case "toString": + return ""; + default: + throw new MockitoException( + join( + "Unexpected overridable method of Object class found", + "", + "The method " + + method + + " was not expected to be declared. Either your JVM build offers " + + "non-official API or the current functionality is not supported", + Platform.describe())); + } + } + RealMethod realMethod; + if (invokeDefault == null || Modifier.isAbstract(method.getModifiers())) { + realMethod = RealMethod.IsIllegal.INSTANCE; + } else { + realMethod = new RealDefaultMethod(proxy, method, args); + } + return handler.get() + .handle( + createInvocation( + proxy, method, args, realMethod, settings, new LocationImpl())); + } + } + + private class RealDefaultMethod implements RealMethod { + + private final Object proxy; + private final SerializableMethod serializableMethod; + private final Object[] args; + + private RealDefaultMethod(Object proxy, Method method, Object[] args) { + this.proxy = proxy; + this.serializableMethod = new SerializableMethod(method); + this.args = args; + } + + @Override + public boolean isInvokable() { + return true; + } + + @Override + public Object invoke() throws Throwable { + try { + return invokeDefault.invoke(null, proxy, serializableMethod.getJavaMethod(), args); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new MockitoException( + join( + "Failed to access default method or invoked method with illegal arguments", + "", + "Method " + + serializableMethod.getJavaMethod() + + " could not be delegated, this is not supposed to happen", + Platform.describe()), + e); + } + } + } +} diff --git a/src/main/java/org/mockito/internal/creation/proxy/package-info.java b/src/main/java/org/mockito/internal/creation/proxy/package-info.java new file mode 100644 index 0000000000..0649d3ba57 --- /dev/null +++ b/src/main/java/org/mockito/internal/creation/proxy/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2007 Mockito contributors + * This program is made available under the terms of the MIT License. + */ + +/** + * Mock makers based on the {@link java.lang.reflect.Proxy} utility. + */ +package org.mockito.internal.creation.proxy; diff --git a/src/test/java/org/mockito/internal/creation/AbstractMockMakerTest.java b/src/test/java/org/mockito/internal/creation/AbstractMockMakerTest.java new file mode 100644 index 0000000000..d59477829a --- /dev/null +++ b/src/test/java/org/mockito/internal/creation/AbstractMockMakerTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2017 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.creation; + +import org.junit.Test; +import org.mockito.internal.handler.MockHandlerImpl; +import org.mockito.internal.stubbing.answers.CallsRealMethods; +import org.mockito.invocation.Invocation; +import org.mockito.invocation.InvocationContainer; +import org.mockito.invocation.MockHandler; +import org.mockito.mock.MockCreationSettings; +import org.mockito.mock.SerializableMode; +import org.mockito.plugins.MockMaker; +import org.mockito.stubbing.Answer; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public abstract class AbstractMockMakerTest { + + protected final MM mockMaker; + + private final Class target; + + protected AbstractMockMakerTest(MM mockMaker, Class target) { + this.mockMaker = mockMaker; + this.target = target; + } + + @Test + public void should_mocks_have_different_interceptors() throws Exception { + C mockOne = mockMaker.createMock(settingsFor(target), dummyHandler()); + C mockTwo = mockMaker.createMock(settingsFor(target), dummyHandler()); + + MockHandler handlerOne = mockMaker.getHandler(mockOne); + MockHandler handlerTwo = mockMaker.getHandler(mockTwo); + + assertThat(handlerOne).isNotSameAs(handlerTwo); + } + + @Test + public void should_reset_mock_and_set_new_handler() throws Throwable { + MockCreationSettings settings = settingsWithSuperCall(target); + C proxy = mockMaker.createMock(settings, new MockHandlerImpl(settings)); + + MockHandler handler = new MockHandlerImpl(settings); + mockMaker.resetMock(proxy, handler, settings); + assertThat(mockMaker.getHandler(proxy)).isSameAs(handler); + } + + protected static MockCreationSettings settingsFor( + Class type, Class... extraInterfaces) { + MockSettingsImpl mockSettings = new MockSettingsImpl(); + mockSettings.setTypeToMock(type); + if (extraInterfaces.length > 0) mockSettings.extraInterfaces(extraInterfaces); + return mockSettings; + } + + protected static MockCreationSettings serializableSettingsFor( + Class type, SerializableMode serializableMode) { + MockSettingsImpl mockSettings = new MockSettingsImpl(); + mockSettings.serializable(serializableMode); + mockSettings.setTypeToMock(type); + return mockSettings; + } + + protected static MockCreationSettings settingsWithConstructorFor(Class type) { + MockSettingsImpl mockSettings = new MockSettingsImpl(); + mockSettings.setTypeToMock(type); + return mockSettings; + } + + protected static MockCreationSettings settingsWithSuperCall(Class type) { + MockSettingsImpl mockSettings = new MockSettingsImpl(); + mockSettings.setTypeToMock(type); + mockSettings.defaultAnswer(new CallsRealMethods()); + return mockSettings; + } + + protected static MockHandler dummyHandler() { + return new DummyMockHandler(); + } + + private static class DummyMockHandler implements MockHandler { + public Object handle(Invocation invocation) throws Throwable { + return null; + } + + public MockCreationSettings getMockSettings() { + return null; + } + + public InvocationContainer getInvocationContainer() { + return null; + } + + public void setAnswersForStubbing(List> list) {} + } +} diff --git a/src/test/java/org/mockito/internal/creation/bytebuddy/AbstractByteBuddyMockMakerTest.java b/src/test/java/org/mockito/internal/creation/bytebuddy/AbstractByteBuddyMockMakerTest.java index 3ddc1817ba..93c8913ace 100644 --- a/src/test/java/org/mockito/internal/creation/bytebuddy/AbstractByteBuddyMockMakerTest.java +++ b/src/test/java/org/mockito/internal/creation/bytebuddy/AbstractByteBuddyMockMakerTest.java @@ -9,31 +9,25 @@ import static org.mockitoutil.ClassLoaders.coverageTool; import java.io.Serializable; -import java.util.List; import net.bytebuddy.ByteBuddy; import org.junit.Test; import org.mockito.Mockito; -import org.mockito.internal.creation.MockSettingsImpl; +import org.mockito.internal.creation.AbstractMockMakerTest; import org.mockito.internal.handler.MockHandlerImpl; -import org.mockito.internal.stubbing.answers.CallsRealMethods; -import org.mockito.invocation.Invocation; -import org.mockito.invocation.InvocationContainer; import org.mockito.invocation.MockHandler; import org.mockito.mock.MockCreationSettings; import org.mockito.mock.SerializableMode; import org.mockito.plugins.MockMaker; -import org.mockito.stubbing.Answer; import org.mockitoutil.ClassLoaders; import org.mockitoutil.SimpleSerializationUtil; import org.objenesis.ObjenesisStd; -public abstract class AbstractByteBuddyMockMakerTest { +public abstract class AbstractByteBuddyMockMakerTest + extends AbstractMockMakerTest { - protected final MM mockMaker; - - public AbstractByteBuddyMockMakerTest(MM mockMaker) { - this.mockMaker = mockMaker; + protected AbstractByteBuddyMockMakerTest(MM mockMaker) { + super(mockMaker, SomeClass.class); } protected abstract Class mockTypeOf(Class type); @@ -69,17 +63,6 @@ public void should_create_mock_from_class_even_when_constructor_is_dodgy() throw assertThat(mock).isNotNull(); } - @Test - public void should_mocks_have_different_interceptors() throws Exception { - SomeClass mockOne = mockMaker.createMock(settingsFor(SomeClass.class), dummyHandler()); - SomeClass mockTwo = mockMaker.createMock(settingsFor(SomeClass.class), dummyHandler()); - - MockHandler handlerOne = mockMaker.getHandler(mockOne); - MockHandler handlerTwo = mockMaker.getHandler(mockTwo); - - assertThat(handlerOne).isNotSameAs(handlerTwo); - } - @Test public void should_use_ancillary_Types() { SomeClass mock = @@ -122,17 +105,6 @@ public void should_create_mock_from_class_with_super_call_to_final_method() thro assertThat(proxy.foo()).isEqualTo("foo"); } - @Test - public void should_reset_mock_and_set_new_handler() throws Throwable { - MockCreationSettings settings = settingsWithSuperCall(SampleClass.class); - SampleClass proxy = - mockMaker.createMock(settings, new MockHandlerImpl(settings)); - - MockHandler handler = new MockHandlerImpl(settings); - mockMaker.resetMock(proxy, handler, settings); - assertThat(mockMaker.getHandler(proxy)).isSameAs(handler); - } - class SomeClass {} interface SomeInterface {} @@ -170,55 +142,6 @@ public void instantiate_fine_when_objenesis_on_the_classpath() throws Exception // then everything went fine } - private static MockCreationSettings settingsFor( - Class type, Class... extraInterfaces) { - MockSettingsImpl mockSettings = new MockSettingsImpl(); - mockSettings.setTypeToMock(type); - if (extraInterfaces.length > 0) mockSettings.extraInterfaces(extraInterfaces); - return mockSettings; - } - - private static MockCreationSettings serializableSettingsFor( - Class type, SerializableMode serializableMode) { - MockSettingsImpl mockSettings = new MockSettingsImpl(); - mockSettings.serializable(serializableMode); - mockSettings.setTypeToMock(type); - return mockSettings; - } - - private static MockCreationSettings settingsWithConstructorFor(Class type) { - MockSettingsImpl mockSettings = new MockSettingsImpl(); - mockSettings.setTypeToMock(type); - return mockSettings; - } - - private static MockCreationSettings settingsWithSuperCall(Class type) { - MockSettingsImpl mockSettings = new MockSettingsImpl(); - mockSettings.setTypeToMock(type); - mockSettings.defaultAnswer(new CallsRealMethods()); - return mockSettings; - } - - protected static MockHandler dummyHandler() { - return new DummyMockHandler(); - } - - private static class DummyMockHandler implements MockHandler { - public Object handle(Invocation invocation) throws Throwable { - return null; - } - - public MockCreationSettings getMockSettings() { - return null; - } - - public InvocationContainer getInvocationContainer() { - return null; - } - - public void setAnswersForStubbing(List> list) {} - } - private static class SampleClass { public String foo() { return "foo"; diff --git a/src/test/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMakerTest.java b/src/test/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMakerTest.java index 161fdd5ccf..e27605640b 100644 --- a/src/test/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMakerTest.java +++ b/src/test/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMakerTest.java @@ -455,7 +455,7 @@ public void test_clear_all_mock_clears_handler() { assertThat(mockMaker.getHandler(proxy2)).isNull(); } - private static MockCreationSettings settingsFor( + protected static MockCreationSettings settingsFor( Class type, Class... extraInterfaces) { MockSettingsImpl mockSettings = new MockSettingsImpl(); mockSettings.setTypeToMock(type); diff --git a/src/test/java/org/mockito/internal/creation/proxy/ProxyMockMakerTest.java b/src/test/java/org/mockito/internal/creation/proxy/ProxyMockMakerTest.java new file mode 100644 index 0000000000..d127a92a13 --- /dev/null +++ b/src/test/java/org/mockito/internal/creation/proxy/ProxyMockMakerTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.creation.proxy; + +import org.junit.Test; +import org.mockito.internal.creation.AbstractMockMakerTest; + +import java.io.Serializable; +import java.lang.reflect.Proxy; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ProxyMockMakerTest + extends AbstractMockMakerTest { + + public ProxyMockMakerTest() { + super(new ProxyMockMaker(), SomeInterface.class); + } + + @Test + public void should_create_mock_from_interface() { + SomeInterface proxy = + mockMaker.createMock(settingsFor(SomeInterface.class), dummyHandler()); + + Class superClass = proxy.getClass().getSuperclass(); + assertThat(superClass).isEqualTo(Proxy.class); + } + + @Test + public void should_create_mock_from_interface_with_extra_interface() { + SomeInterface proxy = + mockMaker.createMock( + settingsFor(SomeInterface.class, Serializable.class), dummyHandler()); + + Class superClass = proxy.getClass().getSuperclass(); + assertThat(superClass).isEqualTo(Proxy.class); + assertThat(proxy).isInstanceOf(Serializable.class); + } + + @Test + public void should_discover_mockable_input() { + assertThat(mockMaker.isTypeMockable(Object.class).mockable()).isFalse(); + assertThat(mockMaker.isTypeMockable(Object.class).nonMockableReason()) + .isEqualTo("non-interface"); + assertThat(mockMaker.isTypeMockable(Object.class).mockable()).isTrue(); + } + + interface SomeInterface {} +}