Skip to content

Commit

Permalink
Add a limited mock maker that is based only on the java.lang.reflect.…
Browse files Browse the repository at this point in the history
…Proxy utility
  • Loading branch information
raphw committed Aug 23, 2021
1 parent b04c703 commit eda775b
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 83 deletions.
Expand Up @@ -21,6 +21,7 @@ class DefaultMockitoPlugins implements MockitoPlugins {

private static final Map<String, String> 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 {
Expand All @@ -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(
Expand Down
172 changes: 172 additions & 0 deletions 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> T createMock(MockCreationSettings<T> 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<MockHandler<?>> 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);
}
}
}
}
@@ -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;
102 changes: 102 additions & 0 deletions 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<MM extends MockMaker, C> {

protected final MM mockMaker;

private final Class<C> target;

protected AbstractMockMakerTest(MM mockMaker, Class<C> 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<C> settings = settingsWithSuperCall(target);
C proxy = mockMaker.createMock(settings, new MockHandlerImpl<C>(settings));

MockHandler handler = new MockHandlerImpl<C>(settings);
mockMaker.resetMock(proxy, handler, settings);
assertThat(mockMaker.getHandler(proxy)).isSameAs(handler);
}

protected static <T> MockCreationSettings<T> settingsFor(
Class<T> type, Class<?>... extraInterfaces) {
MockSettingsImpl<T> mockSettings = new MockSettingsImpl<T>();
mockSettings.setTypeToMock(type);
if (extraInterfaces.length > 0) mockSettings.extraInterfaces(extraInterfaces);
return mockSettings;
}

protected static <T> MockCreationSettings<T> serializableSettingsFor(
Class<T> type, SerializableMode serializableMode) {
MockSettingsImpl<T> mockSettings = new MockSettingsImpl<T>();
mockSettings.serializable(serializableMode);
mockSettings.setTypeToMock(type);
return mockSettings;
}

protected static <T> MockCreationSettings<T> settingsWithConstructorFor(Class<T> type) {
MockSettingsImpl<T> mockSettings = new MockSettingsImpl<T>();
mockSettings.setTypeToMock(type);
return mockSettings;
}

protected static <T> MockCreationSettings<T> settingsWithSuperCall(Class<T> type) {
MockSettingsImpl<T> mockSettings = new MockSettingsImpl<T>();
mockSettings.setTypeToMock(type);
mockSettings.defaultAnswer(new CallsRealMethods());
return mockSettings;
}

protected static MockHandler dummyHandler() {
return new DummyMockHandler();
}

private static class DummyMockHandler implements MockHandler<Object> {
public Object handle(Invocation invocation) throws Throwable {
return null;
}

public MockCreationSettings<Object> getMockSettings() {
return null;
}

public InvocationContainer getInvocationContainer() {
return null;
}

public void setAnswersForStubbing(List<Answer<?>> list) {}
}
}

0 comments on commit eda775b

Please sign in to comment.