Skip to content

Commit

Permalink
MockitoMockMaker provides mocking of final classes and methods (#1753)
Browse files Browse the repository at this point in the history
The MockitoMockMaker provides the ability to mock
final classes and final methods with the InlineMockMaker of Mockito.

fixes #735 and fixes #683
  • Loading branch information
AndreasTu authored Oct 11, 2023
1 parent a40cdff commit 9b1e644
Show file tree
Hide file tree
Showing 20 changed files with 1,097 additions and 53 deletions.
53 changes: 42 additions & 11 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1060,22 +1060,24 @@ The following mock makers are built-in, and are selected in this order:
* `java-proxy`: Uses the `java.lang.reflect.Proxy` API to create mocks of interfaces.
* `byte-buddy`: Uses https://bytebuddy.net/[Byte Buddy] to create mock objects.
** Requires `net.bytebuddy:byte-buddy` 1.9+ on the class path.
* `mockito`: Uses https://site.mockito.org/[Mockito] to create mocks of classes.
** Can be configured to use additional Mockito feature like mock `Serializable`
** Requires `org.mockito:mockito-core` 4.11+ on the class path.
* `cglib`: Deprecated: Uses https://github.com/cglib/cglib[CGLIB] to create mock objects.
** Requires `cglib:cglib-nodep` 3.2.0+ on the class path.


.Capabilities of the different built-in mock makers
[cols=".^~h,^.^15,^.^15,^.^15"]
[cols=".^~h,^.^15,^.^15,^.^15,^.^18"]
|===
^| Capability | `java-proxy` | `byte-buddy` | `cglib`

| Interface | ✔ | ✔ | ✔
| Class | ✘ | ✔ | ✔
| Additional Interfaces | ✔ | ✔ | ✔
| Explicit Constructor Arguments | ✘ | ✔ | ✔
| Final Class | ✘ | ✘ | ✘
| Final Method | ✘ | ✘ | ✘
| Static Method | ✘ | ✘ | ✘
^| Capability | `java-proxy` | `byte-buddy` | `cglib` | `mockito`

| Interface | ✔ | ✔ | ✔ | ✔
| Class | ✘ | ✔ | ✔ | ✔
| Additional Interfaces | ✔ | ✔ | ✔ | ✔
| Explicit Constructor Arguments | ✘ | ✔ | ✔ | ✔
| Final Class | ✘ | ✘ | ✘ | ✔
| Final Method | ✘ | ✘ | ✘ | ✔
| Static Method | ✘ | ✘ | ✘ | ✘
|===

The class `spock.mock.MockMakers` provides constants and methods for the built-in mock makers.
Expand All @@ -1090,6 +1092,35 @@ The preferred mock maker will be used globally, if no mock maker is explicitly s
include::{sourcedir}/extension/MockMakerConfigurationDocSpec.groovy[tag=mock-maker-preferredMockMaker]
----

==== Mockito Mock Maker

The `mockito` Mock Maker provides the ability to mock final types, enums and final methods.
The mocking of final classes is automatically enabled, if `org.mockito:mockito-core` 4.11+ is on the class path.

For mocking of final methods, you need to select `mockito` during mock construction,
like:
[source,groovy,indent=0]
----
include::{sourcedir}/interaction/MockMakerDocSpec.groovy[tag=mockito]
----

If you want to make final method mockable by default, you can select this mock maker as the preferred mock maker.
It can't mock `native` methods, see Mockito for details.

CAUTION: If you try to mock a final method without a Mock Maker supporting it.
It will silently fail, without honoring your specified interactions.

You can configure the created mocks with the `org.mockito.MockSettings` during construction to use features provided by Mockito:

[source,groovy,indent=0]
----
include::{sourcedir}/interaction/MockMakerDocSpec.groovy[tag=mock-serializable]
----

The `mockito` uses the `org.mockito.MockMakers.INLINE` under the hood,
please see the Mockito manual "Mocking final types, enums and final methods" for all pros and cons,
when using `org.mockito.MockMakers.INLINE`.

==== Custom Mock Maker

Spock provides an extension point to plug in your own mock maker for creating mock objects.
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ hamcrest = "org.hamcrest:hamcrest:2.2"
jaxb = "javax.xml.bind:jaxb-api:2.3.1"
junit4 = "junit:junit:4.13.2"
log4j = "log4j:log4j:1.2.17"
mockito4 = "org.mockito:mockito-core:4.11.0"
mockito5 = "org.mockito:mockito-core:5.6.0"
objenesis = "org.objenesis:objenesis:3.3"
# This needs a classifier, but is has to be specified on the usage end https://melix.github.io/blog/2021/03/version-catalogs-faq.html#_why_can_t_i_use_excludes_or_classifiers
jacoco-agent = { module = "org.jacoco:org.jacoco.agent", version.ref = "jacoco" }
Expand Down
2 changes: 2 additions & 0 deletions spock-core/core.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies {
compileOnly libs.bytebuddy
compileOnly libs.cglib
compileOnly libs.objenesis
compileOnly libs.mockito4


coreConsoleRuntime groovyConsoleExtraDependencies
Expand Down Expand Up @@ -68,6 +69,7 @@ tasks.named("jar", Jar) {
'net.bytebuddy.*;resolution:=optional',
'net.sf.cglib.*;resolution:=optional',
'org.objectweb.asm.*;resolution:=optional',
'org.mockito.*;resolution:=optional',
'*'
].join(','),
'-noclassforname': 'true',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public Set<MockMakerCapability> getCapabilities() {

@Override
public int getPriority() {
return 300;
return 400;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.mock.runtime.mockito;

import org.spockframework.mock.CannotCreateMockException;
import org.spockframework.mock.IMockObject;
import org.spockframework.mock.runtime.IMockMaker;
import org.spockframework.util.ReflectionUtil;
import org.spockframework.util.ThreadSafe;

import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

@ThreadSafe
public class MockitoMockMaker implements IMockMaker {
public static final MockMakerId ID = new MockMakerId("mockito");
private static final boolean mockitoAvailable = ReflectionUtil.isClassAvailable("org.mockito.Mockito");
private static final Set<MockMakerCapability> CAPABILITIES = Collections.unmodifiableSet(EnumSet.of(
MockMakerCapability.INTERFACE,
MockMakerCapability.CLASS,
MockMakerCapability.ADDITIONAL_INTERFACES,
MockMakerCapability.EXPLICIT_CONSTRUCTOR_ARGUMENTS,
MockMakerCapability.FINAL_CLASS,
MockMakerCapability.FINAL_METHOD
));

private final MockitoMockMakerImpl impl;

public MockitoMockMaker() {
if (mockitoAvailable) {
this.impl = new MockitoMockMakerImpl();
} else {
this.impl = null;
}
}

@Override
public MockMakerId getId() {
return ID;
}

@Override
public Set<MockMakerCapability> getCapabilities() {
return CAPABILITIES;
}

@Override
public int getPriority() {
return 300;
}

@Override
public IMockObject asMockOrNull(Object object) {
if (impl == null) {
return null;
}
return impl.asMockOrNull(object);
}

@Override
public Object makeMock(IMockCreationSettings settings) throws CannotCreateMockException {
return Objects.requireNonNull(impl).makeMock(settings);
}

@Override
public IMockabilityResult getMockability(IMockCreationSettings settings) {
Class<?> mockType = settings.getMockType();
if (Modifier.isFinal(mockType.getModifiers()) && !settings.getAdditionalInterface().isEmpty()) {
return () -> "Cannot mock final classes with additional interfaces.";
}
if (!mockitoAvailable) {
return () -> "The mockito-core library >= 4.11 is missing on the class path.";
}
return Objects.requireNonNull(impl).isMockable(settings.getMockType());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.mock.runtime.mockito;

import org.mockito.MockMakers;
import org.mockito.MockSettings;
import org.mockito.Mockito;
import org.mockito.MockitoFramework;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.invocation.Invocation;
import org.mockito.invocation.InvocationContainer;
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
import org.mockito.plugins.MockMaker;
import org.spockframework.mock.CannotCreateMockException;
import org.spockframework.mock.IMockObject;
import org.spockframework.mock.ISpockMockObject;
import org.spockframework.mock.MockNature;
import org.spockframework.mock.runtime.IMockMaker;
import org.spockframework.mock.runtime.IProxyBasedMockInterceptor;
import org.spockframework.runtime.GroovyRuntimeUtil;
import org.spockframework.util.ExceptionUtil;
import org.spockframework.util.ObjectUtil;
import org.spockframework.util.ReflectionUtil;
import org.spockframework.util.ThreadSafe;

import java.lang.reflect.Method;

@ThreadSafe
class MockitoMockMakerImpl {
private static final Class<?>[] CLASS_ARRAY = new Class[0];

private final MockMaker inlineMockMaker;
private final Method spockMockMethod;

MockitoMockMakerImpl() {
MockitoFramework framework = Mockito.framework();
this.inlineMockMaker = framework.getPlugins().getInlineMockMaker();
this.spockMockMethod = ReflectionUtil.getMethodByName(ISpockMockObject.class, "$spock_get");
}

IMockObject asMockOrNull(Object object) {
MockHandler<?> mockHandler = inlineMockMaker.getHandler(object);
if (mockHandler instanceof SpockMockHandler) {
SpockMockHandler spockHandler = (SpockMockHandler) mockHandler;
//This should be changed to a better API to retrieve the IMockObject, which is currently implemented in two places JavaMockInterceptor and GroovyMockInterceptor
return (IMockObject) spockHandler.mockInterceptor.intercept(object, spockMockMethod, null, null);
}
return null;
}

Object makeMock(IMockMaker.IMockCreationSettings settings) throws CannotCreateMockException {
try {
MockSettings mockitoSettings = Mockito.withSettings();
mockitoSettings.mockMaker(MockMakers.INLINE);
if (!settings.getAdditionalInterface().isEmpty()) {
mockitoSettings.extraInterfaces(settings.getAdditionalInterface().toArray(CLASS_ARRAY));
}
if (settings.getConstructorArgs() != null) {
mockitoSettings.useConstructor(settings.getConstructorArgs().toArray(GroovyRuntimeUtil.EMPTY_ARGUMENTS));
} else if (settings.getMockNature() == MockNature.SPY) {
//We need to say Mockito it shall use the constructor otherwise it will not initialize fields of the spy, see org.mockito.Mockito.spy(java.lang.Class<T>), which does the same
mockitoSettings.useConstructor();
}

//We do not need the verification logic of Mockito
mockitoSettings.stubOnly();

applyMockMakerSettingsFromUser(settings, mockitoSettings);

MockCreationSettings<Object> mockitoCreationSettings = ObjectUtil.uncheckedCast(mockitoSettings.build(settings.getMockType()));
SpockMockHandler handler = new SpockMockHandler(settings.getMockInterceptor());

return inlineMockMaker.createMock(mockitoCreationSettings, handler);
} catch (MockitoException ex) {
throw new CannotCreateMockException(settings.getMockType(), " with " + MockitoMockMaker.ID + ": " + ex.getMessage().trim(), ex);
}
}

private static void applyMockMakerSettingsFromUser(IMockMaker.IMockCreationSettings settings, MockSettings mockitoSettings) {
IMockMaker.IMockMakerSettings mockMakerSettings = settings.getMockMakerSettings();
if (mockMakerSettings instanceof MockitoMockMakerSettings) {
MockitoMockMakerSettings mockitoMakerSettings = (MockitoMockMakerSettings) mockMakerSettings;
mockitoMakerSettings.applySettings(mockitoSettings);
}
}

public IMockMaker.IMockabilityResult isMockable(Class<?> mockType) {
MockMaker.TypeMockability result = inlineMockMaker.isTypeMockable(mockType);
if (result.mockable()) {
return IMockMaker.IMockabilityResult.MOCKABLE;
}
return result::nonMockableReason;
}

static class SpockMockHandler implements MockHandler<Object> {
private final IProxyBasedMockInterceptor mockInterceptor;

public SpockMockHandler(IProxyBasedMockInterceptor mockInterceptor) {
this.mockInterceptor = mockInterceptor;
}

@Override
public Object handle(Invocation invocation) {
Object mock = invocation.getMock();
return mockInterceptor.intercept(mock, invocation.getMethod(), invocation.getArguments(), spockInvocation -> {
try {
return invocation.callRealMethod();
} catch (Throwable e) {
return ExceptionUtil.sneakyThrow(e);
}
});
}

@Override
public MockCreationSettings<Object> getMockSettings() {
throw new UnsupportedOperationException();
}

@Override
public InvocationContainer getInvocationContainer() {
throw new UnsupportedOperationException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.mock.runtime.mockito;

import groovy.lang.Closure;
import org.mockito.MockSettings;
import org.spockframework.mock.runtime.IMockMaker;
import spock.mock.MockMakers;

import static java.util.Objects.requireNonNull;
import static org.spockframework.mock.runtime.IMockMaker.MockMakerId;

public final class MockitoMockMakerSettings implements IMockMaker.IMockMakerSettings {
private final Closure<?> mockitoCode;

public static MockitoMockMakerSettings createSettings(Closure<?> mockitoCode) {
return new MockitoMockMakerSettings(mockitoCode);
}

private MockitoMockMakerSettings(Closure<?> mockitoCode) {
this.mockitoCode = requireNonNull(mockitoCode);
}

@Override
public MockMakerId getMockMakerId() {
return MockMakers.mockito.getMockMakerId();
}

void applySettings(MockSettings mockSettings) {
requireNonNull(mockSettings);
callClosure(mockitoCode, mockSettings);
}

private static void callClosure(Closure<?> closure, Object delegate) {
Closure<?> settingsClosure = (Closure<?>) closure.clone();
settingsClosure.setResolveStrategy(Closure.DELEGATE_FIRST);
settingsClosure.setDelegate(delegate);
settingsClosure.call(delegate);
}

@Override
public String toString() {
return getMockMakerId().toString();
}
}
Loading

0 comments on commit 9b1e644

Please sign in to comment.