From 3e910ea990e61b29ec76d8b47a6d1b1607d9da3f Mon Sep 17 00:00:00 2001 From: Johannes Spangenberg Date: Wed, 7 Sep 2022 18:01:14 +0200 Subject: [PATCH] Fixes #2626 : Introduce MockSettings.mockMaker (#2701) --- src/main/java/org/mockito/Mock.java | 8 + src/main/java/org/mockito/MockMakers.java | 40 ++++ src/main/java/org/mockito/MockSettings.java | 22 ++ src/main/java/org/mockito/Mockito.java | 23 +- .../org/mockito/internal/MockitoCore.java | 18 +- .../MockAnnotationProcessor.java | 3 + .../configuration/SpyAnnotationEngine.java | 2 + .../injection/SpyOnInjectedFieldsHandler.java | 1 + .../plugins/DefaultMockitoPlugins.java | 20 +- .../plugins/PluginInitializer.java | 8 +- .../configuration/plugins/PluginLoader.java | 8 +- .../configuration/plugins/PluginRegistry.java | 3 +- .../internal/creation/MockSettingsImpl.java | 8 +- .../creation/settings/CreationSettings.java | 7 + .../RetrieveGenericsForDefaultAnswers.java | 7 +- .../defaultanswers/ReturnsDeepStubs.java | 9 +- .../stubbing/defaultanswers/ReturnsMocks.java | 9 +- .../defaultanswers/ReturnsSmartNulls.java | 12 +- .../internal/util/MockCreationValidator.java | 4 +- .../org/mockito/internal/util/MockUtil.java | 85 ++++++-- .../util/reflection/FieldInitializer.java | 6 +- .../mockito/mock/MockCreationSettings.java | 10 + .../java/org/mockito/plugins/MockMaker.java | 14 ++ .../mockito/ProgrammaticMockMakerTest.java | 201 ++++++++++++++++++ .../plugins/DefaultMockitoPluginsTest.java | 8 +- .../util/MockCreationValidatorTest.java | 2 +- .../mockito/internal/util/MockUtilTest.java | 8 +- .../DeferMockMakersClassLoadingTest.java | 96 +++++++++ .../ProgrammaticMockMakerAnnotationTest.java | 42 ++++ .../mockitoinline/ConstructionMockTest.java | 17 ++ 30 files changed, 638 insertions(+), 63 deletions(-) create mode 100644 src/main/java/org/mockito/MockMakers.java create mode 100644 src/test/java/org/mockito/ProgrammaticMockMakerTest.java create mode 100644 src/test/java/org/mockitointegration/DeferMockMakersClassLoadingTest.java create mode 100644 src/test/java/org/mockitousage/annotation/ProgrammaticMockMakerAnnotationTest.java diff --git a/src/main/java/org/mockito/Mock.java b/src/main/java/org/mockito/Mock.java index e8469b830d..2e4f515035 100644 --- a/src/main/java/org/mockito/Mock.java +++ b/src/main/java/org/mockito/Mock.java @@ -13,6 +13,7 @@ import java.lang.annotation.Target; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.plugins.MockMaker; import org.mockito.stubbing.Answer; /** @@ -124,6 +125,13 @@ */ Strictness strictness() default Strictness.TEST_LEVEL_DEFAULT; + /** + * Mock will be created by the given {@link MockMaker}, see {@link MockSettings#mockMaker(String)}. + * + * @since 4.8.0 + */ + String mockMaker() default ""; + enum Strictness { /** diff --git a/src/main/java/org/mockito/MockMakers.java b/src/main/java/org/mockito/MockMakers.java new file mode 100644 index 0000000000..2f5459d222 --- /dev/null +++ b/src/main/java/org/mockito/MockMakers.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito; + +import org.mockito.plugins.MockMaker; + +/** + * Constants for built-in implementations of {@code MockMaker}. + * You may use the constants of this class for {@link MockSettings#mockMaker(String)} or {@link Mock#mockMaker()}. + * The string values of these constants may also be used in the resource file mockito-extensions/org.mockito.plugins.MockMaker + * as described in the class documentation of {@link MockMaker}. + * + * @since 4.8.0 + */ +public final class MockMakers { + /** + * Inline mock maker which can mock final types, enums and final methods. + * This mock maker cannot mock native methods, + * and it does not support {@link MockSettings#extraInterfaces(Class[]) extra interfaces}. + * + * @see Mocking final types, enums and final methods + */ + public static final String INLINE = "mock-maker-inline"; + /** + * Proxy mock maker which avoids code generation, but can only mock interfaces. + * + * @see Avoiding code generation when restricting mocks to interfaces + */ + public static final String PROXY = "mock-maker-proxy"; + /** + * Subclass mock maker which mocks types by creating subclasses. + * This is the first built-in mock maker which has been provided by Mockito. + * Since this mock maker relies on subclasses, it cannot mock final classes and methods. + */ + public static final String SUBCLASS = "mock-maker-subclass"; + + private MockMakers() {} +} diff --git a/src/main/java/org/mockito/MockSettings.java b/src/main/java/org/mockito/MockSettings.java index f838a8d121..8d8a093e25 100644 --- a/src/main/java/org/mockito/MockSettings.java +++ b/src/main/java/org/mockito/MockSettings.java @@ -15,6 +15,7 @@ import org.mockito.listeners.VerificationStartedListener; import org.mockito.mock.MockCreationSettings; import org.mockito.mock.SerializableMode; +import org.mockito.plugins.MockMaker; import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; @@ -381,4 +382,25 @@ public interface MockSettings extends Serializable { * @since 4.6.0 */ MockSettings strictness(Strictness strictness); + + /** + * Specifies the {@code MockMaker} for the mock. + * The default depends on your project as described in the class documentation of {@link MockMaker}. + * You should usually use the default, this option primarily exists to ease migrations. + * You may specify either one of the constants from {@link MockMakers}, + *
+     *     Object mock = Mockito.mock(Object.class, Mockito.withSettings()
+     *             .mockMaker(MockMakers.INLINE));
+     * 
+ * or the {@link Class#getName() binary name} of a class which implements the {@code MockMaker} interface. + *
+     *     Object mock = Mockito.mock(Object.class, Mockito.withSettings()
+     *             .mockMaker("org.awesome.mockito.AwesomeMockMaker"));
+     * 
+ * + * @param mockMaker the {@code MockMaker} to use for the mock + * @return settings instance so that you can fluently specify other settings + * @since 4.8.0 + */ + MockSettings mockMaker(String mockMaker); } diff --git a/src/main/java/org/mockito/Mockito.java b/src/main/java/org/mockito/Mockito.java index 9f7383f232..a221376287 100644 --- a/src/main/java/org/mockito/Mockito.java +++ b/src/main/java/org/mockito/Mockito.java @@ -105,7 +105,8 @@ * 49. New API for mocking object construction (Since 3.5.0)
* 50. Avoiding code generation when restricting mocks to interfaces (Since 3.12.2)
* 51. New API for marking classes as unmockable (Since 4.1.0)
- * 52. New strictness attribute for @Mock annotation and MockSettings.strictness() methods (Since 4.6.0)
+ * 52. New strictness attribute for @Mock annotation and MockSettings.strictness() methods (Since 4.6.0)
+ * 53. Specifying mock maker for individual mocks (Since 4.8.0)
* * *

0. Migrating to Mockito 2

@@ -1586,7 +1587,7 @@ * released. To define mock behavior and to verify method invocations, use the MockedConstruction that is returned. *

* - *

50. Avoiding code generation when only interfaces are mocked (since 3.12.2)

+ *

50. Avoiding code generation when only interfaces are mocked (since 3.12.2)

* * The JVM offers the {@link java.lang.reflect.Proxy} facility for creating dynamic proxies of interface types. For most applications, Mockito * must be capable of mocking classes as supported by the default mock maker, or even final classes, as supported by the inline mock maker. To @@ -1609,7 +1610,7 @@ *

* *

52. - * New strictness attribute for @Mock annotation and MockSettings.strictness() methods (Since 4.6.0)

+ * New strictness attribute for @Mock annotation and MockSettings.strictness() methods (Since 4.6.0) * * You can now customize the strictness level for a single mock, either using `@Mock` annotation strictness attribute or * using `MockSettings.strictness()`. This can be useful if you want all of your mocks to be strict, @@ -1622,6 +1623,22 @@ * Foo mock = Mockito.mock(Foo.class, withSettings().strictness(Strictness.WARN)); * * + *

53. + * Specifying mock maker for individual mocks (Since 4.8.0)

+ * + * You may encounter situations where you want to use a different mock maker for a specific test only. + * For example, you might want to migrate to the inline mock maker, but a few test do not work right away. + * In such case, you can (temporarily) use {@link MockSettings#mockMaker(String)} and {@link Mock#mockMaker()} + * to specify the mock maker for a specific mock which is causing the problem. + * + *

+ *   // using annotation
+ *   @Mock(mockMaker = MockMakers.SUBCLASS)
+ *   Foo mock;
+ *   // using MockSettings.withSettings()
+ *   Foo mock = Mockito.mock(Foo.class, withSettings().mockMaker(MockMakers.SUBCLASS));
+ * 
+ * */ @CheckReturnValue @SuppressWarnings("unchecked") diff --git a/src/main/java/org/mockito/internal/MockitoCore.java b/src/main/java/org/mockito/internal/MockitoCore.java index fff3b7667e..fd39f6a4a2 100644 --- a/src/main/java/org/mockito/internal/MockitoCore.java +++ b/src/main/java/org/mockito/internal/MockitoCore.java @@ -22,7 +22,6 @@ import static org.mockito.internal.util.MockUtil.getMockHandler; import static org.mockito.internal.util.MockUtil.isMock; import static org.mockito.internal.util.MockUtil.resetMock; -import static org.mockito.internal.util.MockUtil.typeMockabilityOf; import static org.mockito.internal.verification.VerificationModeFactory.noInteractions; import static org.mockito.internal.verification.VerificationModeFactory.noMoreInteractions; @@ -31,6 +30,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Function; import org.mockito.InOrder; import org.mockito.MockSettings; @@ -67,10 +67,6 @@ import org.mockito.stubbing.Stubber; import org.mockito.verification.VerificationMode; -import java.util.Arrays; -import java.util.List; -import java.util.function.Function; - @SuppressWarnings("unchecked") public class MockitoCore { @@ -78,10 +74,6 @@ public class MockitoCore { private static final Set> MOCKABLE_CLASSES = Collections.synchronizedSet(new HashSet<>()); - public boolean isTypeMockable(Class typeToMock) { - return typeMockabilityOf(typeToMock).mockable(); - } - public T mock(Class typeToMock, MockSettings settings) { if (!(settings instanceof MockSettingsImpl)) { throw new IllegalArgumentException( @@ -160,6 +152,14 @@ public MockedConstruction mockConstruction( + "At the moment, you cannot provide your own implementations of that class."); } MockSettingsImpl impl = MockSettingsImpl.class.cast(value); + String mockMaker = impl.getMockMaker(); + if (mockMaker != null) { + throw new IllegalArgumentException( + "Unexpected MockMaker '" + + mockMaker + + "'\n" + + "At the moment, you cannot override the MockMaker for construction mocks."); + } return impl.build(typeToMock); }; MockMaker.ConstructionMockControl control = diff --git a/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java b/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java index 66c3f31d12..95356f5c9a 100644 --- a/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java +++ b/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java @@ -53,6 +53,9 @@ public static Object processAnnotationForMock( if (annotation.strictness() != Mock.Strictness.TEST_LEVEL_DEFAULT) { mockSettings.strictness(Strictness.valueOf(annotation.strictness().toString())); } + if (!annotation.mockMaker().isEmpty()) { + mockSettings.mockMaker(annotation.mockMaker()); + } // see @Mock answer default value mockSettings.defaultAnswer(annotation.answer()); diff --git a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java index a88ad63b84..fa9af798f2 100644 --- a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java +++ b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java @@ -83,6 +83,7 @@ public AutoCloseable process(Class context, Object testInstance) { } private static Object spyInstance(Field field, Object instance) { + // TODO: Add mockMaker option for @Spy annotation (#2740) return Mockito.mock( instance.getClass(), withSettings() @@ -93,6 +94,7 @@ private static Object spyInstance(Field field, Object instance) { private static Object spyNewInstance(Object testInstance, Field field) throws InstantiationException, IllegalAccessException, InvocationTargetException { + // TODO: Add mockMaker option for @Spy annotation (#2740) MockSettings settings = withSettings().defaultAnswer(CALLS_REAL_METHODS).name(field.getName()); Class type = field.getType(); diff --git a/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java b/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java index 73f3004c19..bbfe88b85a 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java +++ b/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java @@ -42,6 +42,7 @@ protected boolean processInjection(Field field, Object fieldOwner, Set m // B. protect against multiple use of MockitoAnnotations.openMocks() Mockito.reset(instance); } else { + // TODO: Add mockMaker option for @Spy annotation (#2740) Object mock = Mockito.mock( instance.getClass(), 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 592f79a81e..14f52f3705 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java @@ -5,7 +5,11 @@ package org.mockito.internal.configuration.plugins; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; + +import org.mockito.MockMakers; import org.mockito.plugins.AnnotationEngine; import org.mockito.plugins.DoNotMockEnforcer; import org.mockito.plugins.InstantiatorProvider2; @@ -16,11 +20,13 @@ import org.mockito.plugins.PluginSwitch; import org.mockito.plugins.StackTraceCleanerProvider; -class DefaultMockitoPlugins implements MockitoPlugins { +public 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 INLINE_ALIAS = MockMakers.INLINE; + static final String PROXY_ALIAS = MockMakers.PROXY; + static final String SUBCLASS_ALIAS = MockMakers.SUBCLASS; + public static final Set MOCK_MAKER_ALIASES = new HashSet<>(); static final String MODULE_ALIAS = "member-accessor-module"; static { @@ -41,6 +47,8 @@ class DefaultMockitoPlugins implements MockitoPlugins { 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( + SUBCLASS_ALIAS, "org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker"); DEFAULT_PLUGINS.put( MockitoLogger.class.getName(), "org.mockito.internal.util.ConsoleMockitoLogger"); DEFAULT_PLUGINS.put( @@ -51,6 +59,10 @@ class DefaultMockitoPlugins implements MockitoPlugins { DEFAULT_PLUGINS.put( DoNotMockEnforcer.class.getName(), "org.mockito.internal.configuration.DefaultDoNotMockEnforcer"); + + MOCK_MAKER_ALIASES.add(INLINE_ALIAS); + MOCK_MAKER_ALIASES.add(PROXY_ALIAS); + MOCK_MAKER_ALIASES.add(SUBCLASS_ALIAS); } @Override @@ -59,7 +71,7 @@ public T getDefaultPlugin(Class pluginType) { return create(pluginType, className); } - String getDefaultPluginClass(String classOrAlias) { + public static String getDefaultPluginClass(String classOrAlias) { return DEFAULT_PLUGINS.get(classOrAlias); } 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 a88f0cc19f..b042a84396 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java @@ -18,12 +18,10 @@ class PluginInitializer { private final PluginSwitch pluginSwitch; private final Set alias; - private final DefaultMockitoPlugins plugins; - PluginInitializer(PluginSwitch pluginSwitch, Set alias, DefaultMockitoPlugins plugins) { + PluginInitializer(PluginSwitch pluginSwitch, Set alias) { this.pluginSwitch = pluginSwitch; this.alias = alias; - this.plugins = plugins; } /** @@ -47,7 +45,7 @@ public T loadImpl(Class service) { new PluginFinder(pluginSwitch).findPluginClass(Iterables.toIterable(resources)); if (classOrAlias != null) { if (alias.contains(classOrAlias)) { - classOrAlias = plugins.getDefaultPluginClass(classOrAlias); + classOrAlias = DefaultMockitoPlugins.getDefaultPluginClass(classOrAlias); } Class pluginClass = loader.loadClass(classOrAlias); Object plugin = pluginClass.getDeclaredConstructor().newInstance(); @@ -79,7 +77,7 @@ public List loadImpls(Class service) { List impls = new ArrayList<>(); for (String classOrAlias : classesOrAliases) { if (alias.contains(classOrAlias)) { - classOrAlias = plugins.getDefaultPluginClass(classOrAlias); + classOrAlias = DefaultMockitoPlugins.getDefaultPluginClass(classOrAlias); } Class pluginClass = loader.loadClass(classOrAlias); Object plugin = pluginClass.getDeclaredConstructor().newInstance(); 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 3f33dffdc6..e554173326 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java @@ -27,8 +27,7 @@ class PluginLoader { PluginLoader(PluginSwitch pluginSwitch) { this( new DefaultMockitoPlugins(), - new PluginInitializer( - pluginSwitch, Collections.emptySet(), new DefaultMockitoPlugins())); + new PluginInitializer(pluginSwitch, Collections.emptySet())); } /** @@ -40,10 +39,7 @@ class PluginLoader { PluginLoader(PluginSwitch pluginSwitch, String... alias) { this( new DefaultMockitoPlugins(), - new PluginInitializer( - pluginSwitch, - new HashSet<>(Arrays.asList(alias)), - new DefaultMockitoPlugins())); + new PluginInitializer(pluginSwitch, new HashSet<>(Arrays.asList(alias)))); } /** 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 01e89d2014..6abaedbaef 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java @@ -23,8 +23,7 @@ class PluginRegistry { private final MockMaker mockMaker = new PluginLoader( pluginSwitch, - DefaultMockitoPlugins.INLINE_ALIAS, - DefaultMockitoPlugins.PROXY_ALIAS) + DefaultMockitoPlugins.MOCK_MAKER_ALIASES.toArray(new String[0])) .loadPlugin(MockMaker.class); private final MemberAccessor memberAccessor = diff --git a/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java b/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java index f73a718298..a25299722c 100644 --- a/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java +++ b/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java @@ -254,11 +254,17 @@ public MockSettings strictness(Strictness strictness) { return this; } + @Override + public MockSettings mockMaker(String mockMaker) { + this.mockMaker = mockMaker; + return this; + } + private static CreationSettings validatedSettings( Class typeToMock, CreationSettings source) { MockCreationValidator validator = new MockCreationValidator(); - validator.validateType(typeToMock); + validator.validateType(typeToMock, source.getMockMaker()); validator.validateExtraInterfaces(typeToMock, source.getExtraInterfaces()); validator.validateMockedType(typeToMock, source.getSpiedInstance()); diff --git a/src/main/java/org/mockito/internal/creation/settings/CreationSettings.java b/src/main/java/org/mockito/internal/creation/settings/CreationSettings.java index 13939f6fbd..4b50781833 100644 --- a/src/main/java/org/mockito/internal/creation/settings/CreationSettings.java +++ b/src/main/java/org/mockito/internal/creation/settings/CreationSettings.java @@ -46,6 +46,7 @@ public class CreationSettings implements MockCreationSettings, Serializabl private Object outerClassInstance; private Object[] constructorArgs; protected Strictness strictness = null; + protected String mockMaker; public CreationSettings() {} @@ -68,6 +69,7 @@ public CreationSettings(CreationSettings copy) { this.constructorArgs = copy.getConstructorArgs(); this.strictness = copy.strictness; this.stripAnnotations = copy.stripAnnotations; + this.mockMaker = copy.mockMaker; } @Override @@ -178,4 +180,9 @@ public boolean isLenient() { public Strictness getStrictness() { return strictness; } + + @Override + public String getMockMaker() { + return mockMaker; + } } diff --git a/src/main/java/org/mockito/internal/stubbing/defaultanswers/RetrieveGenericsForDefaultAnswers.java b/src/main/java/org/mockito/internal/stubbing/defaultanswers/RetrieveGenericsForDefaultAnswers.java index fa6a72f53d..8b64a16916 100644 --- a/src/main/java/org/mockito/internal/stubbing/defaultanswers/RetrieveGenericsForDefaultAnswers.java +++ b/src/main/java/org/mockito/internal/stubbing/defaultanswers/RetrieveGenericsForDefaultAnswers.java @@ -8,7 +8,6 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; -import org.mockito.internal.MockitoCore; import org.mockito.internal.util.MockUtil; import org.mockito.internal.util.reflection.GenericMetadataSupport; import org.mockito.invocation.InvocationOnMock; @@ -16,8 +15,6 @@ final class RetrieveGenericsForDefaultAnswers { - private static final MockitoCore MOCKITO_CORE = new MockitoCore(); - static Object returnTypeForMockWithCorrectGenerics( InvocationOnMock invocation, AnswerCallback answerCallback) { Class type = invocation.getMethod().getReturnType(); @@ -38,7 +35,9 @@ static Object returnTypeForMockWithCorrectGenerics( } if (type != null) { - if (!MOCKITO_CORE.isTypeMockable(type)) { + final MockCreationSettings mockSettings = + MockUtil.getMockSettings(invocation.getMock()); + if (!MockUtil.typeMockabilityOf(type, mockSettings.getMockMaker()).mockable()) { return null; } diff --git a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java index 097b11b6e2..8356c1b79b 100644 --- a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java +++ b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java @@ -5,6 +5,7 @@ package org.mockito.internal.stubbing.defaultanswers; import static org.mockito.Mockito.withSettings; +import static org.mockito.internal.util.MockUtil.typeMockabilityOf; import java.io.IOException; import java.io.Serializable; @@ -50,9 +51,10 @@ public Object answer(InvocationOnMock invocation) throws Throwable { GenericMetadataSupport returnTypeGenericMetadata = actualParameterizedType(invocation.getMock()) .resolveGenericReturnType(invocation.getMethod()); + MockCreationSettings mockSettings = MockUtil.getMockSettings(invocation.getMock()); Class rawType = returnTypeGenericMetadata.rawType(); - if (!mockitoCore().isTypeMockable(rawType)) { + if (!typeMockabilityOf(rawType, mockSettings.getMockMaker()).mockable()) { if (invocation.getMethod().getReturnType().equals(rawType)) { return delegate().answer(invocation); } else { @@ -119,7 +121,7 @@ private Object newDeepStubMock( private MockSettings withSettingsUsing( GenericMetadataSupport returnTypeGenericMetadata, - MockCreationSettings parentMockSettings) { + MockCreationSettings parentMockSettings) { MockSettings mockSettings = returnTypeGenericMetadata.hasRawExtraInterfaces() ? withSettings() @@ -127,7 +129,8 @@ private MockSettings withSettingsUsing( : withSettings(); return propagateSerializationSettings(mockSettings, parentMockSettings) - .defaultAnswer(returnsDeepStubsAnswerUsing(returnTypeGenericMetadata)); + .defaultAnswer(returnsDeepStubsAnswerUsing(returnTypeGenericMetadata)) + .mockMaker(parentMockSettings.getMockMaker()); } private MockSettings propagateSerializationSettings( diff --git a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsMocks.java b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsMocks.java index 0ef6f0d954..c155780916 100755 --- a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsMocks.java +++ b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsMocks.java @@ -8,7 +8,9 @@ import org.mockito.Mockito; import org.mockito.internal.creation.MockSettingsImpl; +import org.mockito.internal.util.MockUtil; import org.mockito.invocation.InvocationOnMock; +import org.mockito.mock.MockCreationSettings; import org.mockito.stubbing.Answer; public class ReturnsMocks implements Answer, Serializable { @@ -33,9 +35,14 @@ public Object apply(Class type) { return null; } + MockCreationSettings mockSettings = + MockUtil.getMockSettings(invocation.getMock()); + return Mockito.mock( type, - new MockSettingsImpl().defaultAnswer(ReturnsMocks.this)); + new MockSettingsImpl<>() + .defaultAnswer(ReturnsMocks.this) + .mockMaker(mockSettings.getMockMaker())); } }); } diff --git a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsSmartNulls.java b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsSmartNulls.java index bcca2ef52a..a6e0b2c138 100644 --- a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsSmartNulls.java +++ b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsSmartNulls.java @@ -10,9 +10,12 @@ import java.io.Serializable; import org.mockito.Mockito; +import org.mockito.internal.creation.MockSettingsImpl; import org.mockito.internal.debugging.LocationFactory; +import org.mockito.internal.util.MockUtil; import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.Location; +import org.mockito.mock.MockCreationSettings; import org.mockito.stubbing.Answer; /** @@ -56,9 +59,16 @@ public Object apply(Class type) { return null; } + MockCreationSettings mockSettings = + MockUtil.getMockSettings(invocation.getMock()); + Answer defaultAnswer = + new ThrowsSmartNullPointer(invocation, LocationFactory.create()); + return Mockito.mock( type, - new ThrowsSmartNullPointer(invocation, LocationFactory.create())); + new MockSettingsImpl<>() + .defaultAnswer(defaultAnswer) + .mockMaker(mockSettings.getMockMaker())); } }); } diff --git a/src/main/java/org/mockito/internal/util/MockCreationValidator.java b/src/main/java/org/mockito/internal/util/MockCreationValidator.java index 7baa4e252f..9db6b36ea3 100644 --- a/src/main/java/org/mockito/internal/util/MockCreationValidator.java +++ b/src/main/java/org/mockito/internal/util/MockCreationValidator.java @@ -18,8 +18,8 @@ @SuppressWarnings("unchecked") public class MockCreationValidator { - public void validateType(Class classToMock) { - TypeMockability typeMockability = MockUtil.typeMockabilityOf(classToMock); + public void validateType(Class classToMock, String mockMaker) { + TypeMockability typeMockability = MockUtil.typeMockabilityOf(classToMock, mockMaker); if (!typeMockability.mockable()) { throw cannotMockClass(classToMock, typeMockability.nonMockableReason()); } diff --git a/src/main/java/org/mockito/internal/util/MockUtil.java b/src/main/java/org/mockito/internal/util/MockUtil.java index 2159608623..0d80f6e195 100644 --- a/src/main/java/org/mockito/internal/util/MockUtil.java +++ b/src/main/java/org/mockito/internal/util/MockUtil.java @@ -7,6 +7,7 @@ import org.mockito.MockedConstruction; import org.mockito.Mockito; import org.mockito.exceptions.misusing.NotAMockException; +import org.mockito.internal.configuration.plugins.DefaultMockitoPlugins; import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.creation.settings.CreationSettings; import org.mockito.internal.stubbing.InvocationContainerImpl; @@ -18,6 +19,9 @@ import org.mockito.plugins.MockMaker.TypeMockability; import org.mockito.plugins.MockResolver; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import static org.mockito.internal.handler.MockHandlerFactory.createMockHandler; @@ -25,15 +29,57 @@ @SuppressWarnings("unchecked") public class MockUtil { - private static final MockMaker mockMaker = Plugins.getMockMaker(); + private static final MockMaker defaultMockMaker = Plugins.getMockMaker(); + private static final Map, MockMaker> mockMakers = + new ConcurrentHashMap<>( + Collections.singletonMap(defaultMockMaker.getClass(), defaultMockMaker)); private MockUtil() {} - public static TypeMockability typeMockabilityOf(Class type) { - return mockMaker.isTypeMockable(type); + private static MockMaker getMockMaker(String mockMaker) { + if (mockMaker == null) { + return defaultMockMaker; + } + + String typeName; + if (DefaultMockitoPlugins.MOCK_MAKER_ALIASES.contains(mockMaker)) { + typeName = DefaultMockitoPlugins.getDefaultPluginClass(mockMaker); + } else { + typeName = mockMaker; + } + + Class type; + // Using the context class loader because PluginInitializer.loadImpl is using it as well. + // Personally, I am suspicious whether the context class loader is a good choice in either + // of these cases. + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + if (loader == null) { + loader = ClassLoader.getSystemClassLoader(); + } + try { + type = loader.loadClass(typeName).asSubclass(MockMaker.class); + } catch (Exception e) { + throw new IllegalStateException("Failed to load MockMaker: " + mockMaker, e); + } + + return mockMakers.computeIfAbsent( + type, + t -> { + try { + return t.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException( + "Failed to construct MockMaker: " + t.getName(), e); + } + }); + } + + public static TypeMockability typeMockabilityOf(Class type, String mockMaker) { + return getMockMaker(mockMaker).isTypeMockable(type); } public static T createMock(MockCreationSettings settings) { + MockMaker mockMaker = getMockMaker(settings.getMockMaker()); MockHandler mockHandler = createMockHandler(settings); Object spiedInstance = settings.getSpiedInstance(); @@ -62,17 +108,11 @@ public static void resetMock(Object mock) { MockHandler newHandler = createMockHandler(settings); mock = resolve(mock); - mockMaker.resetMock(mock, newHandler, settings); + getMockMaker(settings.getMockMaker()).resetMock(mock, newHandler, settings); } public static MockHandler getMockHandler(Object mock) { - if (mock == null) { - throw new NotAMockException("Argument should be a mock, but is null!"); - } - - mock = resolve(mock); - - MockHandler handler = mockMaker.getHandler(mock); + MockHandler handler = getMockHandlerOrNull(mock); if (handler != null) { return handler; } else { @@ -104,10 +144,24 @@ public static boolean isMock(Object mock) { if (mock == null) { return false; } + return getMockHandlerOrNull(mock) != null; + } + + private static MockHandler getMockHandlerOrNull(Object mock) { + if (mock == null) { + throw new NotAMockException("Argument should be a mock, but is null!"); + } mock = resolve(mock); - return mockMaker.getHandler(mock) != null; + for (MockMaker mockMaker : mockMakers.values()) { + MockHandler handler = mockMaker.getHandler(mock); + if (handler != null) { + assert getMockMaker(handler.getMockSettings().getMockMaker()) == mockMaker; + return handler; + } + } + return null; } private static Object resolve(Object mock) { @@ -143,6 +197,7 @@ public static MockCreationSettings getMockSettings(Object mock) { public static MockMaker.StaticMockControl createStaticMock( Class type, MockCreationSettings settings) { + MockMaker mockMaker = getMockMaker(settings.getMockMaker()); MockHandler handler = createMockHandler(settings); return mockMaker.createStaticMock(type, settings, handler); } @@ -153,11 +208,13 @@ public static MockMaker.ConstructionMockControl createConstructionMock( MockedConstruction.MockInitializer mockInitializer) { Function> handlerFactory = context -> createMockHandler(settingsFactory.apply(context)); - return mockMaker.createConstructionMock( + return defaultMockMaker.createConstructionMock( type, settingsFactory, handlerFactory, mockInitializer); } public static void clearAllCaches() { - mockMaker.clearAllCaches(); + for (MockMaker mockMaker : mockMakers.values()) { + mockMaker.clearAllCaches(); + } } } diff --git a/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java b/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java index 3f104d1f02..7d51a6bb1b 100644 --- a/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java +++ b/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java @@ -259,7 +259,11 @@ public int compare(Constructor constructorA, Constructor constructorB) { private int countMockableParams(Constructor constructor) { int constructorMockableParamsSize = 0; for (Class aClass : constructor.getParameterTypes()) { - if (MockUtil.typeMockabilityOf(aClass).mockable()) { + // The argResolver already knows the concrete types it can provide. + // Instead of checking for mockability, I think it would be better to + // ask the argResolver whether it can resolve this type. + // Anyway, I keep it for now to avoid breaking any existing code. + if (MockUtil.typeMockabilityOf(aClass, null).mockable()) { constructorMockableParamsSize++; } } diff --git a/src/main/java/org/mockito/mock/MockCreationSettings.java b/src/main/java/org/mockito/mock/MockCreationSettings.java index f7f0b96028..94c74558ee 100644 --- a/src/main/java/org/mockito/mock/MockCreationSettings.java +++ b/src/main/java/org/mockito/mock/MockCreationSettings.java @@ -12,6 +12,7 @@ import org.mockito.listeners.InvocationListener; import org.mockito.listeners.StubbingLookupListener; import org.mockito.listeners.VerificationStartedListener; +import org.mockito.plugins.MockMaker; import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; @@ -135,4 +136,13 @@ public interface MockCreationSettings { * @since 4.6.0 */ Strictness getStrictness(); + + /** + * Returns the {@link MockMaker} which shall be used to create the mock. + * When the return value is {@code null}, the default shall be used. + * + * @see MockSettings#mockMaker(String) + * @since 4.8.0 + */ + String getMockMaker(); } diff --git a/src/main/java/org/mockito/plugins/MockMaker.java b/src/main/java/org/mockito/plugins/MockMaker.java index 93a87ef0a5..c0b1cbcd2d 100644 --- a/src/main/java/org/mockito/plugins/MockMaker.java +++ b/src/main/java/org/mockito/plugins/MockMaker.java @@ -4,6 +4,7 @@ */ package org.mockito.plugins; +import org.mockito.MockSettings; import org.mockito.MockedConstruction; import org.mockito.exceptions.base.MockitoException; import org.mockito.invocation.MockHandler; @@ -45,6 +46,19 @@ *

Note that if several mockito-extensions/org.mockito.plugins.MockMaker files exists in the classpath * Mockito will only use the first returned by the standard {@link ClassLoader#getResource} mechanism. * + *

Using the MockSettings of individual mocks

+ * + *

If you want to use a {@code MockMaker} only for a specific mock, + * you can specify it using {@link MockSettings#mockMaker(String)}.

+ *
+ *     // Use a built-in mock maker
+ *     Object mock = Mockito.mock(Object.class, Mockito.withSettings()
+ *             .mockMaker(MockMakers.INLINE));
+ *     // Or load a mock maker using a fully qualified class name
+ *     Object mock = Mockito.mock(Object.class, Mockito.withSettings()
+ *             .mockMaker("org.awesome.mockito.AwesomeMockMaker"));
+ * 
+ * * @see org.mockito.mock.MockCreationSettings * @see org.mockito.invocation.MockHandler * @since 1.9.5 diff --git a/src/test/java/org/mockito/ProgrammaticMockMakerTest.java b/src/test/java/org/mockito/ProgrammaticMockMakerTest.java new file mode 100644 index 0000000000..f03555d0e5 --- /dev/null +++ b/src/test/java/org/mockito/ProgrammaticMockMakerTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2022 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.withSettings; + +import org.junit.Test; +import org.mockito.exceptions.base.MockitoException; +import org.mockito.exceptions.verification.SmartNullPointerException; +import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker; +import org.mockito.invocation.MockHandler; +import org.mockito.mock.MockCreationSettings; + +public final class ProgrammaticMockMakerTest { + @Test + public void test_normal_mock_uses_given_mock_maker() { + ClassWithFinalMethod inlineMock = + Mockito.mock( + ClassWithFinalMethod.class, withSettings().mockMaker(MockMakers.INLINE)); + ClassWithFinalMethod subclassMock = + Mockito.mock( + ClassWithFinalMethod.class, withSettings().mockMaker(MockMakers.SUBCLASS)); + + Mockito.when(inlineMock.finalMethodCallingNonFinal()).thenReturn("MOCKED"); + Mockito.when(subclassMock.finalMethodCallingNonFinal()).thenReturn("MOCKED"); + + assertEquals("MOCKED", inlineMock.finalMethodCallingNonFinal()); + assertEquals("ORIGINAL", subclassMock.finalMethodCallingNonFinal()); + assertEquals("MOCKED", subclassMock.nonFinal()); + } + + @Test + public void test_mockability_check_uses_given_mock_maker() { + assertNotNull(Mockito.mock(FinalClass.class, withSettings().mockMaker(MockMakers.INLINE))); + assertThrows( + MockitoException.class, + () -> + Mockito.mock( + FinalClass.class, withSettings().mockMaker(MockMakers.SUBCLASS))); + } + + @Test + public void test_deep_stups_inherit_mock_maker() { + Container inlineMock = + Mockito.mock( + Container.class, + withSettings() + .mockMaker(MockMakers.INLINE) + .defaultAnswer(Answers.RETURNS_DEEP_STUBS)); + Container subclassMock = + Mockito.mock( + Container.class, + withSettings() + .mockMaker(MockMakers.SUBCLASS) + .defaultAnswer(Answers.RETURNS_DEEP_STUBS)); + + assertNotNull(inlineMock.finalClass()); + assertNotNull(inlineMock.subContainer().finalClass()); + assertNull(inlineMock.finalClass().someMethod()); + assertNull(inlineMock.subContainer().finalClass().someMethod()); + assertNull(inlineMock.classWithFinalMethod().finalMethod()); + assertNull(inlineMock.subContainer().classWithFinalMethod().finalMethod()); + + assertNull(subclassMock.finalClass()); + assertNull(subclassMock.subContainer().finalClass()); + assertEquals("ORIGINAL", subclassMock.classWithFinalMethod().finalMethod()); + assertEquals("ORIGINAL", subclassMock.subContainer().classWithFinalMethod().finalMethod()); + } + + @Test + public void test_returned_mocks_inherit_mock_maker() { + Container inlineMock = + Mockito.mock( + Container.class, + withSettings() + .mockMaker(MockMakers.INLINE) + .defaultAnswer(Answers.RETURNS_MOCKS)); + Container subclassMock = + Mockito.mock( + Container.class, + withSettings() + .mockMaker(MockMakers.SUBCLASS) + .defaultAnswer(Answers.RETURNS_MOCKS)); + + assertNotNull(inlineMock.finalClass()); + assertNotNull(inlineMock.subContainer().finalClass()); + assertEquals("", inlineMock.finalClass().someMethod()); + assertEquals("", inlineMock.subContainer().finalClass().someMethod()); + assertEquals("", inlineMock.classWithFinalMethod().finalMethod()); + assertEquals("", inlineMock.subContainer().classWithFinalMethod().finalMethod()); + + assertNull(subclassMock.finalClass()); + assertNull(subclassMock.subContainer().finalClass()); + assertEquals("ORIGINAL", subclassMock.classWithFinalMethod().finalMethod()); + assertEquals("ORIGINAL", subclassMock.subContainer().classWithFinalMethod().finalMethod()); + } + + @Test + public void test_smart_nulls_inherit_mock_maker() { + Container inlineMock = + Mockito.mock( + Container.class, + withSettings() + .mockMaker(MockMakers.INLINE) + .defaultAnswer(Answers.RETURNS_SMART_NULLS)); + Container subclassMock = + Mockito.mock( + Container.class, + withSettings() + .mockMaker(MockMakers.SUBCLASS) + .defaultAnswer(Answers.RETURNS_SMART_NULLS)); + + assertNotNull(inlineMock.finalClass()); + assertNotNull(inlineMock.classWithFinalMethod()); + assertThrows(SmartNullPointerException.class, () -> inlineMock.finalClass().someMethod()); + assertThrows( + SmartNullPointerException.class, + () -> inlineMock.classWithFinalMethod().finalMethod()); + + assertNull(subclassMock.finalClass()); + assertNotNull(subclassMock.classWithFinalMethod()); + assertEquals("ORIGINAL", subclassMock.classWithFinalMethod().finalMethod()); + } + + @Test + public void test_custom_mock_maker() { + assertThatThrownBy( + () -> { + Mockito.mock( + Container.class, + withSettings().mockMaker(CustomMockMaker.class.getName())); + }) + .hasMessage("CUSTOM MOCK MAKER"); + } + + @Test + public void test_exception_when_mock_maker_cannot_be_instantiated() { + class InvalidMockMaker extends SubclassByteBuddyMockMaker { + // Local classes have an implicit constructor parameter, + // which makes them an invalid mock maker. + } + assertThatThrownBy( + () -> { + Mockito.mock( + Container.class, + withSettings().mockMaker(InvalidMockMaker.class.getName())); + }) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Failed to construct MockMaker") + .hasMessageContaining(InvalidMockMaker.class.getName()); + } + + private static final class FinalClass { + String someMethod() { + return "ORIGINAL"; + } + } + + private static class ClassWithFinalMethod { + final String finalMethod() { + return "ORIGINAL"; + } + + final String finalMethodCallingNonFinal() { + nonFinal(); + return "ORIGINAL"; + } + + String nonFinal() { + return "ORIGINAL"; + } + } + + private static class Container { + FinalClass finalClass() { + return new FinalClass(); + } + + ClassWithFinalMethod classWithFinalMethod() { + return new ClassWithFinalMethod(); + } + + Container subContainer() { + return new Container(); + } + } + + public static class CustomMockMaker extends SubclassByteBuddyMockMaker { + @Override + public T createMock(MockCreationSettings settings, MockHandler handler) { + throw new RuntimeException("CUSTOM MOCK MAKER"); + } + } +} diff --git a/src/test/java/org/mockito/internal/configuration/plugins/DefaultMockitoPluginsTest.java b/src/test/java/org/mockito/internal/configuration/plugins/DefaultMockitoPluginsTest.java index 44afe0b6a9..aa1835696e 100644 --- a/src/test/java/org/mockito/internal/configuration/plugins/DefaultMockitoPluginsTest.java +++ b/src/test/java/org/mockito/internal/configuration/plugins/DefaultMockitoPluginsTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.*; import static org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.INLINE_ALIAS; import static org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.PROXY_ALIAS; +import static org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.SUBCLASS_ALIAS; import org.junit.Test; import org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker; @@ -25,11 +26,14 @@ public class DefaultMockitoPluginsTest extends TestBase { public void provides_plugins() throws Exception { assertEquals( "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker", - plugins.getDefaultPluginClass(INLINE_ALIAS)); + DefaultMockitoPlugins.getDefaultPluginClass(INLINE_ALIAS)); assertEquals(InlineByteBuddyMockMaker.class, plugins.getInlineMockMaker().getClass()); assertEquals( "org.mockito.internal.creation.proxy.ProxyMockMaker", - plugins.getDefaultPluginClass(PROXY_ALIAS)); + DefaultMockitoPlugins.getDefaultPluginClass(PROXY_ALIAS)); + assertEquals( + "org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker", + DefaultMockitoPlugins.getDefaultPluginClass(SUBCLASS_ALIAS)); assertEquals( ByteBuddyMockMaker.class, plugins.getDefaultPlugin(MockMaker.class).getClass()); assertNotNull(plugins.getDefaultPlugin(InstantiatorProvider2.class)); diff --git a/src/test/java/org/mockito/internal/util/MockCreationValidatorTest.java b/src/test/java/org/mockito/internal/util/MockCreationValidatorTest.java index 7efffcf6a2..7491a3f070 100644 --- a/src/test/java/org/mockito/internal/util/MockCreationValidatorTest.java +++ b/src/test/java/org/mockito/internal/util/MockCreationValidatorTest.java @@ -66,7 +66,7 @@ public void should_validation_be_safe_when_nulls_passed() { @Test public void should_fail_when_type_not_mockable() { try { - validator.validateType(long.class); + validator.validateType(long.class, null); } catch (MockitoException ex) { assertThat(ex.getMessage()).contains("primitive"); } diff --git a/src/test/java/org/mockito/internal/util/MockUtilTest.java b/src/test/java/org/mockito/internal/util/MockUtilTest.java index 50cadb4c0c..834178cdec 100644 --- a/src/test/java/org/mockito/internal/util/MockUtilTest.java +++ b/src/test/java/org/mockito/internal/util/MockUtilTest.java @@ -100,12 +100,12 @@ interface SomeInterface {} @Test public void should_know_if_type_is_mockable() throws Exception { - Assertions.assertThat(MockUtil.typeMockabilityOf(FinalClass.class).mockable()) + Assertions.assertThat(MockUtil.typeMockabilityOf(FinalClass.class, null).mockable()) .isEqualTo(Plugins.getMockMaker().isTypeMockable(FinalClass.class).mockable()); - assertFalse(MockUtil.typeMockabilityOf(int.class).mockable()); + assertFalse(MockUtil.typeMockabilityOf(int.class, null).mockable()); - assertTrue(MockUtil.typeMockabilityOf(SomeClass.class).mockable()); - assertTrue(MockUtil.typeMockabilityOf(SomeInterface.class).mockable()); + assertTrue(MockUtil.typeMockabilityOf(SomeClass.class, null).mockable()); + assertTrue(MockUtil.typeMockabilityOf(SomeInterface.class, null).mockable()); } } diff --git a/src/test/java/org/mockitointegration/DeferMockMakersClassLoadingTest.java b/src/test/java/org/mockitointegration/DeferMockMakersClassLoadingTest.java new file mode 100644 index 0000000000..63c0d761bd --- /dev/null +++ b/src/test/java/org/mockitointegration/DeferMockMakersClassLoadingTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitointegration; + +import static org.mockito.Mockito.withSettings; +import static org.mockitoutil.ClassLoaders.coverageTool; + +import java.lang.reflect.Method; + +import org.assertj.core.api.Assertions; +import org.hamcrest.Matcher; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker; +import org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker; +import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker; +import org.mockito.internal.creation.proxy.ProxyMockMaker; +import org.mockito.invocation.MockHandler; +import org.mockito.mock.MockCreationSettings; +import org.mockito.plugins.MockMaker; +import org.mockitoutil.ClassLoaders; + +public class DeferMockMakersClassLoadingTest { + private static final Object MY_MOCK = new Object(); + + @Test + public void mockito_should_not_load_mock_makers_it_does_not_need() throws Exception { + ClassLoader classLoader_without_mockMakers = + ClassLoaders.excludingClassLoader() + .withCodeSourceUrlOf( + Mockito.class, + Matcher.class, + CustomMockMaker.class, + Assertions.class) + .withCodeSourceUrlOf(coverageTool()) + .without( + ByteBuddyMockMaker.class.getName(), + SubclassByteBuddyMockMaker.class.getName(), + InlineByteBuddyMockMaker.class.getName(), + ProxyMockMaker.class.getName()) + .build(); + + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader_without_mockMakers); + try { + Class self = classLoader_without_mockMakers.loadClass(getClass().getName()); + Method createMock = self.getMethod("createMock"); + createMock.invoke(null); + } finally { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + + // Called by reflection from the test method + public static void createMock() { + Assertions.assertThat( + Mockito.mock( + Object.class, + withSettings().mockMaker(CustomMockMaker.class.getName()))) + .isSameAs(MY_MOCK); + } + + public static class CustomMockMaker implements MockMaker { + @Override + public T createMock(MockCreationSettings settings, MockHandler handler) { + return settings.getTypeToMock().cast(MY_MOCK); + } + + @Override + public MockHandler getHandler(Object mock) { + throw new UnsupportedOperationException(); + } + + @Override + public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) { + throw new UnsupportedOperationException(); + } + + @Override + public TypeMockability isTypeMockable(Class type) { + return new TypeMockability() { + @Override + public boolean mockable() { + return type.equals(Object.class); + } + + @Override + public String nonMockableReason() { + return mockable() ? "" : "type != Object.class"; + } + }; + } + } +} diff --git a/src/test/java/org/mockitousage/annotation/ProgrammaticMockMakerAnnotationTest.java b/src/test/java/org/mockitousage/annotation/ProgrammaticMockMakerAnnotationTest.java new file mode 100644 index 0000000000..ad5f2a50ae --- /dev/null +++ b/src/test/java/org/mockitousage/annotation/ProgrammaticMockMakerAnnotationTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitousage.annotation; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockMakers; +import org.mockito.Mockito; +import org.mockitoutil.TestBase; + +public class ProgrammaticMockMakerAnnotationTest extends TestBase { + @Mock(mockMaker = MockMakers.INLINE) + ClassWithFinalMethod inlineMock; + + @Mock(mockMaker = MockMakers.SUBCLASS) + ClassWithFinalMethod subclassMock; + + @Test + public void test_mock_uses_given_mock_maker() { + Mockito.when(inlineMock.finalMethodCallingNonFinal()).thenReturn("MOCKED"); + Mockito.when(subclassMock.finalMethodCallingNonFinal()).thenReturn("MOCKED"); + + assertEquals("MOCKED", inlineMock.finalMethodCallingNonFinal()); + assertEquals("ORIGINAL", subclassMock.finalMethodCallingNonFinal()); + assertEquals("MOCKED", subclassMock.nonFinal()); + } + + private static class ClassWithFinalMethod { + final String finalMethodCallingNonFinal() { + nonFinal(); + return "ORIGINAL"; + } + + String nonFinal() { + return "ORIGINAL"; + } + } +} diff --git a/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockTest.java b/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockTest.java index 7eb87c5702..adbe3fe3ee 100644 --- a/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockTest.java +++ b/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockTest.java @@ -11,11 +11,13 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; +import org.mockito.MockMakers; import org.mockito.MockedConstruction; import org.mockito.Mockito; import org.mockito.exceptions.base.MockitoException; @@ -154,6 +156,21 @@ public void testConstructionMockMustNotTargetAbstractClass() { .hasMessageContaining("It is not possible to construct primitive types or abstract types"); } + @Test + public void testConstructionMocksMustNotUseCustomMockMaker() { + assertThatThrownBy( + () -> { + try (MockedConstruction ignored = Mockito.mockConstruction( + Dummy.class, + withSettings().mockMaker(MockMakers.INLINE)) + ) { + new Dummy(); + } + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("you cannot override the MockMaker for construction mocks"); + } + static class Dummy {