diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index d52a9c7324..5722942ff4 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -26,6 +26,7 @@ include::include.adoc[] * Size of data providers is no longer calculated multiple times but only once * Fix exception when using `@RepeatUntilFailure` with a data provider with unknown iteration amount. spockPull:2031[] * Clarified documentation about data providers and `size()` calls spockIssue:2022[] +* Add best-effort error reporting for interactions on final methods when using the `byte-buddy` mock maker spockIssue:2039[] == 2.4-M4 (2024-03-21) diff --git a/spock-core/src/main/java/org/spockframework/compiler/AstUtil.java b/spock-core/src/main/java/org/spockframework/compiler/AstUtil.java index 61c1287c40..9ada98adba 100644 --- a/spock-core/src/main/java/org/spockframework/compiler/AstUtil.java +++ b/spock-core/src/main/java/org/spockframework/compiler/AstUtil.java @@ -40,7 +40,6 @@ */ public abstract class AstUtil { private static final Pattern DATA_TABLE_SEPARATOR = Pattern.compile("_{2,}+"); - private static final String GET_METHOD_NAME = "get"; private static final String GET_AT_METHOD_NAME = new IntegerArrayGetAtMetaMethod().getName(); /** diff --git a/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java b/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java index 3b02908707..9fb14f8b81 100644 --- a/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java +++ b/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java @@ -17,6 +17,7 @@ package org.spockframework.compiler; import org.spockframework.compiler.model.*; +import org.spockframework.runtime.GroovyRuntimeUtil; import org.spockframework.runtime.SpockException; import org.spockframework.util.*; @@ -26,7 +27,6 @@ import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.*; -import org.codehaus.groovy.runtime.MetaClassHelper; import org.codehaus.groovy.syntax.*; import org.objectweb.asm.Opcodes; @@ -109,7 +109,7 @@ private void changeFinalFieldInternalName(Field field) { } private void createSharedFieldGetter(Field field) { - String getterName = "get" + MetaClassHelper.capitalize(field.getName()); + String getterName = GroovyRuntimeUtil.propertyToGetterMethodName(field.getName()); MethodNode getter = spec.getAst().getMethod(getterName, Parameter.EMPTY_ARRAY); if (getter != null) { errorReporter.error(field.getAst(), @@ -135,7 +135,7 @@ private void createSharedFieldGetter(Field field) { } private void createFinalFieldGetter(Field field) { - String getterName = "get" + MetaClassHelper.capitalize(field.getName()); + String getterName = GroovyRuntimeUtil.propertyToGetterMethodName(field.getName()); MethodNode getter = spec.getAst().getMethod(getterName, Parameter.EMPTY_ARRAY); if (getter != null) { errorReporter.error(field.getAst(), @@ -158,7 +158,7 @@ private void createFinalFieldGetter(Field field) { } private void createSharedFieldSetter(Field field) { - String setterName = "set" + MetaClassHelper.capitalize(field.getName()); + String setterName = GroovyRuntimeUtil.propertyToSetterMethodName(field.getName()); Parameter[] params = new Parameter[] { new Parameter(field.getAst().getType(), "$spock_value") }; MethodNode setter = spec.getAst().getMethod(setterName, params); if (setter != null) { diff --git a/spock-core/src/main/java/org/spockframework/compiler/SpockNames.java b/spock-core/src/main/java/org/spockframework/compiler/SpockNames.java index aa4edf9024..181aeb4b8b 100644 --- a/spock-core/src/main/java/org/spockframework/compiler/SpockNames.java +++ b/spock-core/src/main/java/org/spockframework/compiler/SpockNames.java @@ -1,8 +1,18 @@ package org.spockframework.compiler; +import org.spockframework.mock.ISpockMockObject; + public class SpockNames { public static final String VALUE_RECORDER = "$spock_valueRecorder"; public static final String ERROR_COLLECTOR = "$spock_errorCollector"; public static final String OLD_VALUE = "$spock_oldValue"; public static final String SPOCK_EX = "$spock_ex"; + /** + * Name of the method {@link ISpockMockObject#$spock_get()}. + */ + public static final String SPOCK_GET = "$spock_get"; + /** + * Name of the method {@link ISpockMockObject#$spock_mockInteractionValidator()}. + */ + public static final String SPOCK_MOCK_INTERATION_VALIDATOR = "$spock_mockInteractionValidator"; } diff --git a/spock-core/src/main/java/org/spockframework/mock/IMockObject.java b/spock-core/src/main/java/org/spockframework/mock/IMockObject.java index 44407da2e8..4157ec5c29 100644 --- a/spock-core/src/main/java/org/spockframework/mock/IMockObject.java +++ b/spock-core/src/main/java/org/spockframework/mock/IMockObject.java @@ -15,6 +15,7 @@ package org.spockframework.mock; import org.spockframework.mock.runtime.SpecificationAttachable; +import org.spockframework.util.Beta; import org.spockframework.util.Nullable; import spock.lang.Specification; @@ -29,6 +30,13 @@ public interface IMockObject extends SpecificationAttachable { @Nullable String getName(); + /** + * Returns the {@link #getName()} of this mock object, or {@code "unnamed"} if it has no name. + * + * @return the name of this mock object, or {@code "unnamed"} if it has no name + */ + String getMockName(); + /** * Returns the declared type of this mock object. * @@ -81,4 +89,12 @@ public interface IMockObject extends SpecificationAttachable { * @return whether this mock object matches the target of the specified interaction */ boolean matches(Object target, IMockInteraction interaction); + + /** + * Returns the used mock configuration which created this mock. + * + * @return the mock configuration + */ + @Beta + IMockConfiguration getConfiguration(); } diff --git a/spock-core/src/main/java/org/spockframework/mock/ISpockMockObject.java b/spock-core/src/main/java/org/spockframework/mock/ISpockMockObject.java index c9e1595960..4ddd72002a 100644 --- a/spock-core/src/main/java/org/spockframework/mock/ISpockMockObject.java +++ b/spock-core/src/main/java/org/spockframework/mock/ISpockMockObject.java @@ -14,10 +14,28 @@ package org.spockframework.mock; +import org.spockframework.mock.runtime.IMockInteractionValidator; +import org.spockframework.util.Beta; +import org.spockframework.util.Nullable; + /** - * Marker-like interface implemented by all mock objects that allows - * {@link MockUtil} to detect mock objects. Not intended for direct use. + * MockObject interface implemented by some {@link spock.mock.MockMakers} that allows the {@link org.spockframework.mock.runtime.MockMakerRegistry} + * to detect mock objects. + * + *

Not intended for direct use. */ public interface ISpockMockObject { + IMockObject $spock_get(); + + /** + * @return the {@link IMockInteractionValidator} used to verify {@link IMockInteraction} + * @see IMockInteractionValidator + * @since 2.4 + */ + @Nullable + @Beta + default IMockInteractionValidator $spock_mockInteractionValidator() { + return null; + } } diff --git a/spock-core/src/main/java/org/spockframework/mock/MockUtil.java b/spock-core/src/main/java/org/spockframework/mock/MockUtil.java index 745525cd80..aa2e943fd1 100644 --- a/spock-core/src/main/java/org/spockframework/mock/MockUtil.java +++ b/spock-core/src/main/java/org/spockframework/mock/MockUtil.java @@ -35,7 +35,7 @@ public class MockUtil { * @return whether the given object is a (Spock) mock object */ public boolean isMock(Object object) { - return getMockMakerRegistry().asMockOrNull(object) != null; + return asMockOrNull(object) != null; } /** @@ -69,6 +69,17 @@ public IMockObject asMock(Object object) { return mockOrNull; } + /** + * Returns information about a mock object or {@code null}, if the object is not a mock. + * + * @param object a mock object + * @return information about the mock object or {@code null}, if the object is not a mock. + */ + @Nullable + public IMockObject asMockOrNull(Object object) { + return getMockMakerRegistry().asMockOrNull(object); + } + /** * Attaches mock to a Specification. * diff --git a/spock-core/src/main/java/org/spockframework/mock/constraint/EqualMethodNameConstraint.java b/spock-core/src/main/java/org/spockframework/mock/constraint/EqualMethodNameConstraint.java index 020120e600..82196a538f 100644 --- a/spock-core/src/main/java/org/spockframework/mock/constraint/EqualMethodNameConstraint.java +++ b/spock-core/src/main/java/org/spockframework/mock/constraint/EqualMethodNameConstraint.java @@ -31,6 +31,10 @@ public EqualMethodNameConstraint(String methodName) { this.methodName = methodName; } + public String getMethodName() { + return methodName; + } + @Override public boolean isSatisfiedBy(IMockInvocation invocation) { return invocation.getMethod().getName().equals(methodName); diff --git a/spock-core/src/main/java/org/spockframework/mock/constraint/EqualPropertyNameConstraint.java b/spock-core/src/main/java/org/spockframework/mock/constraint/EqualPropertyNameConstraint.java index 737916baef..eb21660dff 100644 --- a/spock-core/src/main/java/org/spockframework/mock/constraint/EqualPropertyNameConstraint.java +++ b/spock-core/src/main/java/org/spockframework/mock/constraint/EqualPropertyNameConstraint.java @@ -29,6 +29,10 @@ public EqualPropertyNameConstraint(String propertyName) { this.propertyName = propertyName; } + public String getPropertyName() { + return propertyName; + } + @Override public boolean isSatisfiedBy(IMockInvocation invocation) { return propertyName.equals(getPropertyName(invocation)); diff --git a/spock-core/src/main/java/org/spockframework/mock/constraint/TargetConstraint.java b/spock-core/src/main/java/org/spockframework/mock/constraint/TargetConstraint.java index 5df131f027..b6e3753ac4 100644 --- a/spock-core/src/main/java/org/spockframework/mock/constraint/TargetConstraint.java +++ b/spock-core/src/main/java/org/spockframework/mock/constraint/TargetConstraint.java @@ -17,6 +17,7 @@ package org.spockframework.mock.constraint; import org.spockframework.mock.*; +import org.spockframework.mock.runtime.IMockInteractionValidator; import org.spockframework.runtime.Condition; import org.spockframework.runtime.InvalidSpecException; import org.spockframework.util.CollectionUtil; @@ -50,12 +51,27 @@ public String describeMismatch(IMockInvocation invocation) { @Override public void setInteraction(IMockInteraction interaction) { this.interaction = interaction; - if (interaction.isRequired() && MOCK_UTIL.isMock(target)) { - IMockObject mockObject = MOCK_UTIL.asMock(target); + IMockObject mockObject = MOCK_UTIL.asMockOrNull(target); + if (mockObject != null) { + checkRequiredInteraction(mockObject, interaction); + validateMockInteraction(mockObject, interaction); + } + } + + private void checkRequiredInteraction(IMockObject mockObject, IMockInteraction interaction) { + if (interaction.isRequired()) { if (!mockObject.isVerified()) { - String mockName = mockObject.getName() != null ? mockObject.getName() : "unnamed"; throw new InvalidSpecException("Stub '%s' matches the following required interaction:" + - "\n\n%s\n\nRemove the cardinality (e.g. '1 *'), or turn the stub into a mock.\n").withArgs(mockName, interaction); + "\n\n%s\n\nRemove the cardinality (e.g. '1 *'), or turn the stub into a mock.\n").withArgs(mockObject.getMockName(), interaction); + } + } + } + + private void validateMockInteraction(IMockObject mockObject, IMockInteraction interaction) { + if (target instanceof ISpockMockObject) { + IMockInteractionValidator validation = ((ISpockMockObject) target).$spock_mockInteractionValidator(); + if (validation != null) { + validation.validateMockInteraction(mockObject, interaction); } } } diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/BaseMockInterceptor.java b/spock-core/src/main/java/org/spockframework/mock/runtime/BaseMockInterceptor.java index 3af8948528..a4e0bf5510 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/BaseMockInterceptor.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/BaseMockInterceptor.java @@ -1,5 +1,8 @@ package org.spockframework.mock.runtime; +import org.spockframework.compiler.SpockNames; +import org.spockframework.mock.IMockObject; +import org.spockframework.mock.ISpockMockObject; import org.spockframework.runtime.GroovyRuntimeUtil; import java.lang.reflect.Method; @@ -8,8 +11,13 @@ import groovy.lang.*; import org.jetbrains.annotations.Nullable; +import org.spockframework.util.ReflectionUtil; + +import static java.util.Objects.requireNonNull; public abstract class BaseMockInterceptor implements IProxyBasedMockInterceptor { + + private MetaClass mockMetaClass; BaseMockInterceptor(MetaClass mockMetaClass) { @@ -39,11 +47,11 @@ protected String handleGetProperty(GroovyObject target, Object[] args) { MetaClass metaClass = target.getMetaClass(); //First try the isXXX before getXXX, because this is the expected behavior, if it is boolean property. MetaMethod booleanVariant = metaClass - .getMetaMethod(GroovyRuntimeUtil.propertyToMethodName("is", propertyName), GroovyRuntimeUtil.EMPTY_ARGUMENTS); + .getMetaMethod(GroovyRuntimeUtil.propertyToBooleanGetterMethodName(propertyName), GroovyRuntimeUtil.EMPTY_ARGUMENTS); if (booleanVariant != null && booleanVariant.getReturnType() == boolean.class) { methodName = booleanVariant.getName(); } else { - methodName = GroovyRuntimeUtil.propertyToMethodName("get", propertyName); + methodName = GroovyRuntimeUtil.propertyToGetterMethodName(propertyName); } } return methodName; @@ -52,4 +60,11 @@ protected String handleGetProperty(GroovyObject target, Object[] args) { protected boolean isMethod(Method method, String name, Class... parameterTypes) { return method.getName().equals(name) && Arrays.equals(method.getParameterTypes(), parameterTypes); } + + public static Object handleSpockMockInterface(Method method, IMockObject mockObject) { + if (method.getName().equals(SpockNames.SPOCK_MOCK_INTERATION_VALIDATOR)) { + return null; //This should be handled in the corresponding MockMakers. + } + return mockObject; + } } diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java index 0e4086aa5f..1d174489ad 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java @@ -22,6 +22,7 @@ import net.bytebuddy.TypeCache; import net.bytebuddy.description.annotation.AnnotationDescription; import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.modifier.MethodManifestation; import net.bytebuddy.description.modifier.SynchronizationState; import net.bytebuddy.description.modifier.SyntheticState; import net.bytebuddy.description.modifier.Visibility; @@ -31,19 +32,23 @@ import net.bytebuddy.dynamic.loading.MultipleParentClassLoader; import net.bytebuddy.dynamic.scaffold.TypeValidation; import net.bytebuddy.implementation.FieldAccessor; +import net.bytebuddy.implementation.FixedValue; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.bind.annotation.Morph; import org.codehaus.groovy.runtime.callsite.AbstractCallSite; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; +import org.spockframework.compiler.SpockNames; import org.spockframework.mock.ISpockMockObject; import org.spockframework.mock.codegen.Target; +import org.spockframework.util.ReflectionUtil; import java.lang.reflect.Method; import java.util.Collection; import java.util.concurrent.Callable; import java.util.concurrent.ThreadLocalRandom; +import static java.util.Objects.requireNonNull; import static net.bytebuddy.matcher.ElementMatchers.none; class ByteBuddyMockFactory { @@ -60,6 +65,7 @@ class ByteBuddyMockFactory { private static final Class CODEGEN_TARGET_CLASS = Target.class; private static final String CODEGEN_PACKAGE = CODEGEN_TARGET_CLASS.getPackage().getName(); private static final AnnotationDescription INTERNAL_ANNOTATION = AnnotationDescription.Builder.ofType(Internal.class).build(); + private static final Method MOCK_INTERACTION_VALIDATOR_METHOD = requireNonNull(ReflectionUtil.getMethodByName(ISpockMockObject.class, SpockNames.SPOCK_MOCK_INTERATION_VALIDATOR)); /** * This array contains {@link TypeCachingLock} instances, which are used as java monitor locks for @@ -136,6 +142,10 @@ Object createMock(IMockMaker.IMockCreationSettings settings) { .method(m -> !isGroovyMOPMethod(type, m)) .intercept(mockInterceptor()) .transform(mockTransformer()) + .method(m -> m.represents(MOCK_INTERACTION_VALIDATOR_METHOD)) + // Implement the $spock_mockInteractionValidation() method which returns the static field below, so we have a validation instance for every mock class + .intercept(FixedValue.reference(new ByteBuddyMockInteractionValidator(), SpockNames.SPOCK_MOCK_INTERATION_VALIDATOR)) + .transform(validateMockInteractionTransformer()) .implement(ByteBuddyInterceptorAdapter.InterceptorAccess.class) .intercept(FieldAccessor.ofField("$spock_interceptor")) .defineField("$spock_interceptor", IProxyBasedMockInterceptor.class, Visibility.PRIVATE, SyntheticState.SYNTHETIC) @@ -149,6 +159,10 @@ Object createMock(IMockMaker.IMockCreationSettings settings) { return proxy; } + private static Transformer validateMockInteractionTransformer() { + return Transformer.ForMethod.withModifiers(SynchronizationState.PLAIN, Visibility.PUBLIC, MethodManifestation.FINAL); + } + private static Transformer mockTransformer() { return Transformer.ForMethod.withModifiers(SynchronizationState.PLAIN, Visibility.PUBLIC); //Overridden methods should be public and non-synchronized. } diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockInteractionValidator.java b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockInteractionValidator.java new file mode 100644 index 0000000000..8af3a96868 --- /dev/null +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockInteractionValidator.java @@ -0,0 +1,134 @@ +/* + * Copyright 2024 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; + +import org.junit.platform.commons.support.ModifierSupport; +import org.spockframework.mock.IInvocationConstraint; +import org.spockframework.mock.IMockInteraction; +import org.spockframework.mock.IMockObject; +import org.spockframework.mock.MockImplementation; +import org.spockframework.mock.constraint.EqualMethodNameConstraint; +import org.spockframework.mock.constraint.EqualPropertyNameConstraint; +import org.spockframework.runtime.InvalidSpecException; +import org.spockframework.util.ThreadSafe; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; +import static org.spockframework.runtime.GroovyRuntimeUtil.propertyToBooleanGetterMethodName; +import static org.spockframework.runtime.GroovyRuntimeUtil.propertyToGetterMethodName; + +/** + * {@link ByteBuddyMockInteractionValidator} validates the {@link IMockInteraction} for {@link ByteBuddyMockMaker} mocks. + * + *

+ * + *

Implementation note: The {@link ByteBuddyMockFactory} create a single instance for each mocked class + * and the same validation is used for multiple mocks of the same class. + * + * @author Andreas Turban + */ +@ThreadSafe +final class ByteBuddyMockInteractionValidator implements IMockInteractionValidator { + + private volatile Class mockClass; + private volatile Set finalMethods; + + ByteBuddyMockInteractionValidator() { + } + + @Override + public void validateMockInteraction(IMockObject mockObject, IMockInteraction mockInteractionParam) { + requireNonNull(mockObject); + requireNonNull(mockInteractionParam); + Object instance = requireNonNull(mockObject.getInstance()); + + if (mockObject.getConfiguration().getImplementation() == MockImplementation.GROOVY) { + //We do not validate final methods for Groovy mocks, because final mocking can be done with the Groovy MOP. + return; + } + + initializeClassData(instance); + + validate(mockObject, (MockInteraction) mockInteractionParam); + } + + private void validate(IMockObject mockObject, MockInteraction mockInteraction) { + for (IInvocationConstraint constraint : mockInteraction.getConstraints()) { + if (constraint instanceof EqualMethodNameConstraint) { + EqualMethodNameConstraint methodConstraint = (EqualMethodNameConstraint) constraint; + String methodName = methodConstraint.getMethodName(); + validateNonFinalMethod(mockObject, methodName); + } + if (constraint instanceof EqualPropertyNameConstraint) { + EqualPropertyNameConstraint propNameConstraint = (EqualPropertyNameConstraint) constraint; + validateProperty(mockObject, propNameConstraint); + } + } + } + + private void validateProperty(IMockObject mockObject, EqualPropertyNameConstraint propNameConstraint) { + String propName = propNameConstraint.getPropertyName(); + //We do not need to check for setters, because a property access like x.prop = value has not mock interaction syntax + validateNonFinalMethod(mockObject, propertyToGetterMethodName(propName)); + validateNonFinalMethod(mockObject, propertyToBooleanGetterMethodName(propName)); + } + + private void validateNonFinalMethod(IMockObject mockObject, String methodName) { + if (finalMethods.contains(methodName)) { + throw new InvalidSpecException("The final method '" + + methodName + "' of '" + + mockObject.getMockName() + + "' can't be mocked by the '" + + ByteBuddyMockMaker.ID + + "' mock maker. Please use another mock maker supporting final methods."); + } + } + + private void initializeClassData(Object mock) { + Class mockClassOfMockObj = mock.getClass(); + if (mockClass == null) { + synchronized (this) { + if (mockClass == null) { + finalMethods = Arrays.stream(mockClassOfMockObj.getMethods()) + .filter(m -> !ModifierSupport.isStatic(m)) + .collect(Collectors.groupingBy(Method::getName)) + .entrySet() + .stream() + .filter(e -> e.getValue() + .stream() + .allMatch(ModifierSupport::isFinal)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + + mockClass = mockClassOfMockObj; + } + } + } + + if (mockClass != mockClassOfMockObj) { + throw new IllegalStateException(); + } + } +} diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockInterceptor.java b/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockInterceptor.java index 48d46fd64b..9e1fcfdd45 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockInterceptor.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockInterceptor.java @@ -36,11 +36,10 @@ public GroovyMockInterceptor(IMockConfiguration mockConfiguration, Specification @Override public Object intercept(Object target, Method method, Object[] arguments, IResponseGenerator realMethodInvoker) { - IMockObject mockObject = new MockObject(mockConfiguration.getName(), mockConfiguration.getExactType(), target, - mockConfiguration.isVerified(), mockConfiguration.isGlobal(), mockConfiguration.getDefaultResponse(), specification, this); + IMockObject mockObject = new MockObject(mockConfiguration, target, specification, this); if (method.getDeclaringClass() == ISpockMockObject.class) { - return mockObject; + return handleSpockMockInterface(method, mockObject); } // we do not need the cast information from the wrappers here, the method selection @@ -61,7 +60,7 @@ public Object intercept(Object target, Method method, Object[] arguments, IRespo } } if (isMethod(method, "setProperty", String.class, Object.class)) { - String methodName = GroovyRuntimeUtil.propertyToMethodName("set", (String) args[0]); + String methodName = GroovyRuntimeUtil.propertyToSetterMethodName((String) args[0]); return GroovyRuntimeUtil.invokeMethod(target, methodName, args[1]); } if (isMethod(method, "methodMissing", String.class, Object.class)) { diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockMetaClass.java b/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockMetaClass.java index b8ef479d79..1af4f4e1a7 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockMetaClass.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockMetaClass.java @@ -53,17 +53,17 @@ public Object invokeConstructor(Object[] arguments) { @Override public Object getProperty(Object target, String property) { - String methodName = GroovyRuntimeUtil.propertyToMethodName("is", property); + String methodName = GroovyRuntimeUtil.propertyToBooleanGetterMethodName(property); MetaMethod metaMethod = delegate.getMetaMethod(methodName, GroovyRuntimeUtil.EMPTY_ARGUMENTS); if (metaMethod == null || metaMethod.getReturnType() != boolean.class) { - methodName = GroovyRuntimeUtil.propertyToMethodName("get", property); + methodName = GroovyRuntimeUtil.propertyToGetterMethodName(property); } return invokeMethod(target, methodName, GroovyRuntimeUtil.EMPTY_ARGUMENTS); } @Override public void setProperty(Object target, String property, Object newValue) { - String methodName = GroovyRuntimeUtil.propertyToMethodName("set", property); + String methodName = GroovyRuntimeUtil.propertyToSetterMethodName(property); invokeMethod(target, methodName, new Object[] {newValue}); } @@ -121,8 +121,7 @@ private boolean isGetMetaClassCallOnGroovyObject(Object target, String method, O private IMockInvocation createMockInvocation(MetaMethod metaMethod, Object target, String methodName, Object[] arguments, boolean isStatic) { - IMockObject mockObject = new MockObject(configuration.getName(), configuration.getExactType(), target, - configuration.isVerified(), configuration.isGlobal(), configuration.getDefaultResponse(), specification, this); + IMockObject mockObject = new MockObject(configuration, target, specification, this); IMockMethod mockMethod; if (metaMethod != null) { List parameterTypes = asList(metaMethod.getNativeParameterTypes()); diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/IMockInteractionValidator.java b/spock-core/src/main/java/org/spockframework/mock/runtime/IMockInteractionValidator.java new file mode 100644 index 0000000000..474d08a907 --- /dev/null +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/IMockInteractionValidator.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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; + +import org.spockframework.mock.IMockInteraction; +import org.spockframework.mock.IMockObject; +import org.spockframework.mock.ISpockMockObject; +import org.spockframework.util.Beta; +import org.spockframework.util.ThreadSafe; + +/** + * The {@link IMockInteractionValidator} interface allows {@link IMockMaker} implementations + * to implement different kinds of validations on mock interactions. + * + *

The instance is registered and the {@link ISpockMockObject#$spock_mockInteractionValidator()} method. The {@code IMockMaker} shall return the validation instance to use.

+ * + * @author Andreas Turban + * @since 2.4 + */ +@ThreadSafe +@Beta +public interface IMockInteractionValidator { + /** + * Is called by the {@link IMockInteraction} on creation to allow the underlying {@link IMockMaker} to validate the interaction. + * + * @param mockObject the spock mock object + * @param interaction the interaction to validate + * @throws org.spockframework.runtime.InvalidSpecException if the interface is invalid + * @since 2.4 + */ + @Beta + void validateMockInteraction(IMockObject mockObject, IMockInteraction interaction); +} diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java b/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java index a90eacb7dc..125b66e5e9 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java @@ -37,11 +37,10 @@ public JavaMockInterceptor(IMockConfiguration mockConfiguration, Specification s @Override public Object intercept(Object target, Method method, Object[] arguments, IResponseGenerator realMethodInvoker) { - IMockObject mockObject = new MockObject(mockConfiguration.getName(), mockConfiguration.getExactType(), - target, mockConfiguration.isVerified(), false, mockConfiguration.getDefaultResponse(), specification, this); + IMockObject mockObject = new MockObject(mockConfiguration, target, specification, this); if (method.getDeclaringClass() == ISpockMockObject.class) { - return mockObject; + return handleSpockMockInterface(method, mockObject); } // here no instances of org.codehaus.groovy.runtime.wrappers.Wrapper subclasses @@ -64,7 +63,7 @@ public Object intercept(Object target, Method method, Object[] arguments, IRespo // HACK: for some reason, runtime dispatches direct property access on mock classes via ScriptBytecodeAdapter // delegate to the corresponding setter method // for abstract groovy classes and interfaces it uses InvokerHelper - String methodName = GroovyRuntimeUtil.propertyToMethodName("set", (String)args[0]); + String methodName = GroovyRuntimeUtil.propertyToSetterMethodName((String) args[0]); return GroovyRuntimeUtil.invokeMethod(target, methodName, GroovyRuntimeUtil.asArgumentArray(args[1])); } } diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/MockInteraction.java b/spock-core/src/main/java/org/spockframework/mock/runtime/MockInteraction.java index 4971e59033..c197e86b43 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/MockInteraction.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/MockInteraction.java @@ -71,6 +71,10 @@ public boolean matches(IMockInvocation invocation) { return true; } + List getConstraints() { + return constraints; + } + @Override public Supplier accept(IMockInvocation invocation) { acceptedInvocations.add(invocation); diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/MockObject.java b/spock-core/src/main/java/org/spockframework/mock/runtime/MockObject.java index ab98ebc0a8..adbb621d51 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/MockObject.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/MockObject.java @@ -23,25 +23,18 @@ import java.lang.reflect.Type; +import static java.util.Objects.*; + public class MockObject implements IMockObject { - private final String name; - private final Type type; + private final IMockConfiguration configuration; private final Object instance; - private final boolean verified; - private final boolean global; - private final IDefaultResponse defaultResponse; private final SpecificationAttachable mockInterceptor; - + @Nullable private Specification specification; - public MockObject(@Nullable String name, Type type, Object instance, boolean verified, boolean global, - IDefaultResponse defaultResponse, Specification specification, SpecificationAttachable mockInterceptor) { - this.name = name; - this.type = type; + public MockObject(IMockConfiguration configuration, Object instance, Specification specification, SpecificationAttachable mockInterceptor) { + this.configuration = requireNonNull(configuration); this.instance = instance; - this.verified = verified; - this.global = global; - this.defaultResponse = defaultResponse; this.specification = specification; this.mockInterceptor = mockInterceptor; } @@ -49,17 +42,22 @@ public MockObject(@Nullable String name, Type type, Object instance, boolean ver @Override @Nullable public String getName() { - return name; + return configuration.getName(); + } + + @Override + public String getMockName() { + return getName() != null ? getName() : "unnamed"; } @Override public Class getType() { - return GenericTypeReflectorUtil.erase(type); + return GenericTypeReflectorUtil.erase(getExactType()); } @Override public Type getExactType() { - return type; + return configuration.getExactType(); } @Override @@ -69,12 +67,16 @@ public Object getInstance() { @Override public boolean isVerified() { - return verified; + return configuration.isVerified(); + } + + private boolean isGlobal() { + return configuration.isGlobal(); } @Override public IDefaultResponse getDefaultResponse() { - return defaultResponse; + return configuration.getDefaultResponse(); } @Override @@ -84,9 +86,9 @@ public Specification getSpecification() { @Override public boolean matches(Object target, IMockInteraction interaction) { - if (target instanceof Wildcard) return verified || !interaction.isRequired(); + if (target instanceof Wildcard) return isVerified() || !interaction.isRequired(); - boolean match = global ? matchGlobal(target) : instance == target; + boolean match = isGlobal() ? matchGlobal(target) : instance == target; if (match) { checkRequiredInteractionAllowed(interaction); } @@ -102,10 +104,9 @@ private boolean isMockOfClass() { } private void checkRequiredInteractionAllowed(IMockInteraction interaction) { - if (!verified && interaction.isRequired()) { - String mockName = name != null ? name : "unnamed"; + if (!isVerified() && interaction.isRequired()) { throw new InvalidSpecException("Stub '%s' matches the following required interaction:" + - "\n\n%s\n\nRemove the cardinality (e.g. '1 *'), or turn the stub into a mock.\n").withArgs(mockName, interaction); + "\n\n%s\n\nRemove the cardinality (e.g. '1 *'), or turn the stub into a mock.\n").withArgs(getMockName(), interaction); } } @@ -120,4 +121,9 @@ public void detach() { this.specification = null; this.mockInterceptor.detach(); } + + @Override + public IMockConfiguration getConfiguration() { + return configuration; + } } diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/mockito/MockitoMockMakerImpl.java b/spock-core/src/main/java/org/spockframework/mock/runtime/mockito/MockitoMockMakerImpl.java index dea6f9b283..fdb7027c58 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/mockito/MockitoMockMakerImpl.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/mockito/MockitoMockMakerImpl.java @@ -29,6 +29,7 @@ import org.mockito.mock.MockCreationSettings; import org.mockito.plugins.MockMaker; import org.mockito.plugins.MockitoPlugins; +import org.spockframework.compiler.SpockNames; import org.spockframework.mock.*; import org.spockframework.mock.runtime.IMockMaker; import org.spockframework.mock.runtime.IProxyBasedMockInterceptor; @@ -53,7 +54,7 @@ class MockitoMockMakerImpl { MockitoMockMakerImpl() { inlineMockMaker = resolveInlineMockMaker(); - spockMockMethod = ReflectionUtil.getMethodByName(ISpockMockObject.class, "$spock_get"); + spockMockMethod = ReflectionUtil.getMethodByName(ISpockMockObject.class, SpockNames.SPOCK_GET); } /** diff --git a/spock-core/src/main/java/org/spockframework/runtime/GroovyRuntimeUtil.java b/spock-core/src/main/java/org/spockframework/runtime/GroovyRuntimeUtil.java index c9a7cd34fa..3686e1eb30 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/GroovyRuntimeUtil.java +++ b/spock-core/src/main/java/org/spockframework/runtime/GroovyRuntimeUtil.java @@ -37,6 +37,10 @@ * @author Peter Niederwieser */ public abstract class GroovyRuntimeUtil { + private static final String SET = "set"; + private static final String GET = "get"; + private static final String IS = "is"; + public static Object[] EMPTY_ARGUMENTS = new Object[0]; public static boolean isTruthy(Object obj) { @@ -101,6 +105,18 @@ public static String propertyToMethodName(String prefix, String propertyName) { return prefix + MetaClassHelper.capitalize(propertyName); } + public static String propertyToGetterMethodName(String propertyName) { + return propertyToMethodName(GET, propertyName); + } + + public static String propertyToSetterMethodName(String propertyName) { + return propertyToMethodName(SET, propertyName); + } + + public static String propertyToBooleanGetterMethodName(String propertyName) { + return propertyToMethodName(IS, propertyName); + } + /** * Checks if the given method is a getter method according * to Groovy rules. If yes, the corresponding property name @@ -115,10 +131,10 @@ public static String getterMethodToPropertyName(String methodName, List if (!parameterTypes.isEmpty()) return null; if (returnType == void.class) return null; // Void.class is allowed - if (methodName.startsWith("get")) + if (methodName.startsWith(GET)) return getPropertyName(methodName, 3); - if (methodName.startsWith("is") + if (methodName.startsWith(IS) && returnType == boolean.class) // Boolean.class is not allowed return getPropertyName(methodName, 2); diff --git a/spock-specs/src/test/groovy/org/spockframework/runtime/GroovyRuntimeUtilSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/runtime/GroovyRuntimeUtilSpec.groovy index 6b7b2a148f..59f25844de 100644 --- a/spock-specs/src/test/groovy/org/spockframework/runtime/GroovyRuntimeUtilSpec.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/runtime/GroovyRuntimeUtilSpec.groovy @@ -15,7 +15,11 @@ package org.spockframework.runtime import org.codehaus.groovy.runtime.typehandling.GroovyCastException -import spock.lang.* +import spock.lang.Specification + +import static org.spockframework.runtime.GroovyRuntimeUtil.propertyToBooleanGetterMethodName +import static org.spockframework.runtime.GroovyRuntimeUtil.propertyToGetterMethodName +import static org.spockframework.runtime.GroovyRuntimeUtil.propertyToSetterMethodName class GroovyRuntimeUtilSpec extends Specification { def "getterMethodToPropertyName"() { @@ -39,10 +43,26 @@ class GroovyRuntimeUtilSpec extends Specification { "get" | Integer | null "is" | boolean | null "foo" | String | null + "isfoo" | String | null "setFoo" | void | null } + def "propertyToGetterMethodName"() { + expect: + propertyToGetterMethodName("prop") == "getProp" + } + + def "propertyToSetterMethodName"() { + expect: + propertyToSetterMethodName("prop") == "setProp" + } + + def "propertyToBooleanGetterMethodName"() { + expect: + propertyToBooleanGetterMethodName("prop") == "isProp" + } + def "coerce"() { expect: GroovyRuntimeUtil.coerce("x", Character) == "x" as Character diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaMocks.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaMocks.groovy new file mode 100644 index 0000000000..22e77192ca --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaMocks.groovy @@ -0,0 +1,189 @@ +/* + * Copyright 2024 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.smoke.mock + +import org.spockframework.runtime.InvalidSpecException +import spock.lang.Issue +import spock.lang.Specification +import spock.mock.MockMakers + +class JavaMocks extends Specification { + + def "can mock final classes"() { + when: + def person = Mock(FinalPerson) + person.phoneNumber >> 6789 + + then: + person.phoneNumber == "6789" + } + + def "can mock final methods as property with mockito"() { + FinalMethodPerson person = Mock(mockMaker: MockMakers.mockito) + person.phoneNumber >> 6789 + + expect: + person.phoneNumber == "6789" + } + + def "can mock final methods with mockito"() { + given: + FinalMethodPerson person = Mock(mockMaker: MockMakers.mockito) + person.getPhoneNumber() >> 6789 + + expect: + person.getPhoneNumber() == "6789" + } + + def "can mock final methods with mockito with closure"() { + given: + FinalMethodPerson person = Mock(mockMaker: MockMakers.mockito) { + phoneNumber >> 6789 + } + + expect: + person.phoneNumber == "6789" + } + + def "can mock final methods with mockito with closure and specified type"() { + FinalMethodPerson person = Mock(FinalMethodPerson, mockMaker: MockMakers.mockito) { + phoneNumber >> 6789 + } + + expect: + person.phoneNumber == "6789" + } + + @Issue("https://github.com/spockframework/spock/issues/2039") + def "cannot mock final methods with byteBuddy"() { + given: + FinalMethodPerson person = Mock(mockMaker: MockMakers.byteBuddy) + + when: + person.getPhoneNumber() >> 6789 + + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'getPhoneNumber' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." + + expect: + person.getPhoneNumber() == "12345" + } + + @Issue("https://github.com/spockframework/spock/issues/2039") + def "cannot mock final methods with byteBuddy without error message when one overload is non final"() { + given: + FinalMethodPerson person = Mock(mockMaker: MockMakers.byteBuddy) + + person.finalAndNonFinalOverload() >> "B" + + expect: + person.finalAndNonFinalOverload() == "final" + } + + @Issue("https://github.com/spockframework/spock/issues/2039") + def "non final method overload shall be mockable"() { + given: + FinalMethodPerson person = Mock(mockMaker: MockMakers.byteBuddy) + + person.finalAndNonFinalOverload("A") >> "B" + + expect: + person.finalAndNonFinalOverload("A") == "B" + } + + def "cannot mock final method as property with byteBuddy"() { + given: + FinalMethodPerson person = Mock(mockMaker: MockMakers.byteBuddy) + + when: + person.phoneNumber >> 6789 + + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'getPhoneNumber' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." + + expect: + person.phoneNumber == "12345" + } + + def "cannot mock final is getter as property with byteBuddy"() { + given: + FinalMethodPerson person = Mock(mockMaker: MockMakers.byteBuddy) + + when: + person.finalPerson >> false + + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'isFinalPerson' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." + + expect: + person.finalPerson + } + + def "cannot mock final methods without specifying mockMaker"() { + given: + FinalMethodPerson person = Mock() + + when: + person.getPhoneNumber() >> 6789 + + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'getPhoneNumber' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." + } + + def "no static type specified"() { + when: + Mock() + + then: + InvalidSpecException ex = thrown() + ex.message == "Mock object type cannot be inferred automatically. Please specify a type explicitly (e.g. 'Mock(Person)')." + } + + interface IPerson { + String getName() + + int getAge() + } + + static class Person implements IPerson { + String name = "default" + int age + } + + @SuppressWarnings('GrMethodMayBeStatic') + static final class FinalPerson extends Person { + String getPhoneNumber() { "12345" } + } + + @SuppressWarnings('GrMethodMayBeStatic') + static class FinalMethodPerson extends Person { + + final String getPhoneNumber() { "12345" } + + final boolean isFinalPerson() { return true } + + final String finalAndNonFinalOverload() { + return "final" + } + + String finalAndNonFinalOverload(String arg) { + return arg + } + } +} diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaSpies.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaSpies.groovy index 30f451a896..b6c5c5da7c 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaSpies.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaSpies.groovy @@ -185,7 +185,7 @@ class JavaSpies extends Specification { person.phoneNumber == "6789" } - def "can spy final methods with mockito"() { + def "can spy final methods as property with mockito"() { FinalMethodPerson person = Spy(mockMaker: MockMakers.mockito) person.phoneNumber >> 6789 @@ -193,6 +193,14 @@ class JavaSpies extends Specification { person.phoneNumber == "6789" } + def "can spy final methods with mockito"() { + FinalMethodPerson person = Spy(mockMaker: MockMakers.mockito) + person.getPhoneNumber() >> 6789 + + expect: + person.getPhoneNumber() == "6789" + } + def "can spy final methods with mockito with closure"() { FinalMethodPerson person = Spy(mockMaker: MockMakers.mockito) { phoneNumber >> 6789 @@ -202,22 +210,45 @@ class JavaSpies extends Specification { person.phoneNumber == "6789" } + @Issue("https://github.com/spockframework/spock/issues/2039") def "cannot spy final methods with byteBuddy"() { FinalMethodPerson person = Spy(mockMaker: MockMakers.byteBuddy) + + when: + person.getPhoneNumber() >> 6789 + + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'getPhoneNumber' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." + + expect: + person.getPhoneNumber() == "12345" + } + + @Issue("https://github.com/spockframework/spock/issues/2039") + def "cannot spy final methods as property with byteBuddy"() { + FinalMethodPerson person = Spy(mockMaker: MockMakers.byteBuddy) + + when: person.phoneNumber >> 6789 + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'getPhoneNumber' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." + expect: person.phoneNumber == "12345" } - def "cannot spy on final methods without specifying mockMaker"() { + def "cannot spy final methods without specifying mockMaker"() { FinalMethodPerson person = Spy() when: - person.phoneNumber + person.phoneNumber >> 6789 then: - 0 * person.phoneNumber + InvalidSpecException ex = thrown() + ex.message == "The final method 'getPhoneNumber' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." } def "cannot spy globally"() { diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaStubs.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaStubs.groovy index 9e70d29cd9..28eb933736 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaStubs.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/JavaStubs.groovy @@ -133,7 +133,7 @@ class JavaStubs extends Specification { person.phoneNumber == "6789" } - def "can stub final methods with mockito"() { + def "can stub final methods as property with mockito"() { FinalMethodPerson person = Stub(mockMaker: MockMakers.mockito) person.phoneNumber >> 6789 @@ -141,6 +141,15 @@ class JavaStubs extends Specification { person.phoneNumber == "6789" } + def "can stub final methods with mockito"() { + given: + FinalMethodPerson person = Mock(mockMaker: MockMakers.mockito) + person.getPhoneNumber() >> 6789 + + expect: + person.getPhoneNumber() == "6789" + } + def "can stub final methods with mockito with closure"() { given: FinalMethodPerson person = Stub(mockMaker: MockMakers.mockito) { @@ -160,20 +169,84 @@ class JavaStubs extends Specification { person.phoneNumber == "6789" } + @Issue("https://github.com/spockframework/spock/issues/2039") def "cannot stub final methods with byteBuddy"() { + given: + FinalMethodPerson person = Stub(mockMaker: MockMakers.byteBuddy) + + when: + person.getPhoneNumber() >> 6789 + + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'getPhoneNumber' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." + + expect: + person.getPhoneNumber() == "12345" + } + + @Issue("https://github.com/spockframework/spock/issues/2039") + def "cannot stub final methods with byteBuddy without error message when one overload is non final"() { + given: + FinalMethodPerson person = Stub(mockMaker: MockMakers.byteBuddy) + + person.finalAndNonFinalOverload() >> "B" + + expect: + person.finalAndNonFinalOverload() == "final" + } + + @Issue("https://github.com/spockframework/spock/issues/2039") + def "non final method overload shall be stubable"() { + given: FinalMethodPerson person = Stub(mockMaker: MockMakers.byteBuddy) + + person.finalAndNonFinalOverload("A") >> "B" + + expect: + person.finalAndNonFinalOverload("A") == "B" + } + + def "cannot stub final method as property with byteBuddy"() { + given: + FinalMethodPerson person = Stub(mockMaker: MockMakers.byteBuddy) + + when: person.phoneNumber >> 6789 + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'getPhoneNumber' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." + expect: person.phoneNumber == "12345" } + def "cannot stub final is getter as property with byteBuddy"() { + given: + FinalMethodPerson person = Stub(mockMaker: MockMakers.byteBuddy) + + when: + person.finalPerson >> false + + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'isFinalPerson' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." + + expect: + person.finalPerson + } + def "cannot stub final methods without specifying mockMaker"() { + given: FinalMethodPerson person = Stub() - person.phoneNumber >> 6789 - expect: - person.phoneNumber == "12345" + when: + person.getPhoneNumber() >> 6789 + + then: + InvalidSpecException ex = thrown() + ex.message == "The final method 'getPhoneNumber' of 'person' can't be mocked by the 'byte-buddy' mock maker. Please use another mock maker supporting final methods." } def "cannot stub globally"() { @@ -208,11 +281,24 @@ class JavaStubs extends Specification { List children } + @SuppressWarnings('GrMethodMayBeStatic') static final class FinalPerson extends Person { String getPhoneNumber() { "12345" } } + @SuppressWarnings('GrMethodMayBeStatic') static class FinalMethodPerson extends Person { + final String getPhoneNumber() { "12345" } + + final boolean isFinalPerson() { return true } + + final String finalAndNonFinalOverload() { + return "final" + } + + String finalAndNonFinalOverload(String arg) { + return arg + } } } diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/MockDefaultResponses.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/MockDefaultResponses.groovy index 4a075816bc..b6001e8e9a 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/MockDefaultResponses.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/MockDefaultResponses.groovy @@ -16,6 +16,8 @@ package org.spockframework.smoke.mock +import org.spockframework.mock.IMockObject +import org.spockframework.mock.ISpockMockObject import spock.lang.Specification class MockDefaultResponses extends Specification { @@ -72,6 +74,16 @@ class MockDefaultResponses extends Specification { cmock.getDouble().getClass() == Double.class } + def "default impl of ISpockMockObject.spock_mockInteractionValidation"() { + given: + def obj = new ISpockMockObject() { + @Override + IMockObject $spock_get() { return null } + } + expect: + obj.$spock_mockInteractionValidator() == null + } + interface IMockable { boolean getBoolean() byte getByte() diff --git a/spock-spring/src/main/java/org/spockframework/spring/mock/DelegatingInterceptor.java b/spock-spring/src/main/java/org/spockframework/spring/mock/DelegatingInterceptor.java index 29eae58721..ef5739fbec 100644 --- a/spock-spring/src/main/java/org/spockframework/spring/mock/DelegatingInterceptor.java +++ b/spock-spring/src/main/java/org/spockframework/spring/mock/DelegatingInterceptor.java @@ -17,6 +17,7 @@ package org.spockframework.spring.mock; import org.spockframework.mock.*; +import org.spockframework.mock.runtime.BaseMockInterceptor; import org.spockframework.mock.runtime.IProxyBasedMockInterceptor; import org.spockframework.runtime.model.FieldInfo; import org.spockframework.util.*; @@ -35,7 +36,7 @@ public DelegatingInterceptor(FieldInfo fieldInfo) { @Override public Object intercept(Object target, Method method, Object[] arguments, IResponseGenerator realMethodInvoker) { if (method.getDeclaringClass() == ISpockMockObject.class) { - return new MockObjectAdapter(this); + return BaseMockInterceptor.handleSpockMockInterface(method, new MockObjectAdapter(this)); } if (delegate == null) { @@ -91,6 +92,11 @@ public String getName() { return null; } + @Override + public String getMockName() { + return null; + } + @Override public Class getType() { return null; @@ -125,5 +131,10 @@ public Specification getSpecification() { public boolean matches(Object target, IMockInteraction interaction) { return false; } + + @Override + public IMockConfiguration getConfiguration() { + throw new UnsupportedOperationException(); + } } }