diff --git a/spock-core/src/main/java/org/spockframework/mock/IMockConfiguration.java b/spock-core/src/main/java/org/spockframework/mock/IMockConfiguration.java index 337b15fc7e..8bff772008 100644 --- a/spock-core/src/main/java/org/spockframework/mock/IMockConfiguration.java +++ b/spock-core/src/main/java/org/spockframework/mock/IMockConfiguration.java @@ -88,6 +88,14 @@ public interface IMockConfiguration { */ IDefaultResponse getDefaultResponse(); + /** + * Tells whether this mock object supports last defined return value. + * By default Spock uses a match first algorithm to determine the defined return value of a method. + * + * @return whether this mock object supports last matched response + */ + boolean useLastMatchResponseStrategy(); + /** * Tells whether a mock object stands in for all objects of the mocked type, or just for itself. * This is an optional feature that may not be supported by a particular {@link MockImplementation}. 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 b7f77fb46e..e1a37fd852 100644 --- a/spock-core/src/main/java/org/spockframework/mock/IMockObject.java +++ b/spock-core/src/main/java/org/spockframework/mock/IMockObject.java @@ -65,6 +65,14 @@ public interface IMockObject extends SpecificationAttachable { */ IDefaultResponse getDefaultResponse(); + /** + * Tells whether this mock object supports last defined return value. + * By default Spock uses a match first algorithm to determine the defined return value of a method. + * + * @return whether this mock object supports last matched response + */ + boolean useLastMatchResponseStrategy(); + /** * Returns the specification that this mock object is attached to. * 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 b3eba8d312..ae864a6332 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 @@ -37,7 +37,8 @@ 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); + mockConfiguration.isVerified(), mockConfiguration.isGlobal(), mockConfiguration.getDefaultResponse(), + mockConfiguration.useLastMatchResponseStrategy(), specification, this); if (method.getDeclaringClass() == ISpockMockObject.class) { return mockObject; 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 44e31a2ff0..3d2751da6b 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 @@ -112,8 +112,10 @@ 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.getName(), configuration.getExactType(), target, configuration.isVerified(), + configuration.isGlobal(), configuration.getDefaultResponse(), configuration.useLastMatchResponseStrategy(), + specification, this); IMockMethod mockMethod; if (metaMethod != null) { List parameterTypes = Arrays.asList(metaMethod.getNativeParameterTypes()); diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/InteractionScope.java b/spock-core/src/main/java/org/spockframework/mock/runtime/InteractionScope.java index d15e1a2ffe..44f4097f23 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/InteractionScope.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/InteractionScope.java @@ -77,6 +77,14 @@ public void addUnmatchedInvocation(IMockInvocation invocation) { @Override public IMockInteraction match(IMockInvocation invocation) { + final IMockObject mockObject = invocation.getMockObject(); + if (mockObject.useLastMatchResponseStrategy()) + return lastMatchStrategy(invocation); + + return firstMatchStrategy(invocation); + } + + private IMockInteraction firstMatchStrategy(IMockInvocation invocation) { IMockInteraction firstMatch = null; for (IMockInteraction interaction : interactions) if (interaction.matches(invocation)) { @@ -87,6 +95,17 @@ public IMockInteraction match(IMockInvocation invocation) { return firstMatch; } + private IMockInteraction lastMatchStrategy(IMockInvocation invocation) { + IMockInteraction lastMatch = null; + for (IMockInteraction interaction : interactions) + if (interaction.matches(invocation)) { + if (!interaction.isExhausted() || lastMatch == null) + lastMatch = interaction; + } + + return lastMatch; + } + @Override public void verifyInteractions() { List unsatisfiedInteractions = new ArrayList<>(); 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 bf23327875..776cb38879 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 @@ -38,7 +38,8 @@ 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); + target, mockConfiguration.isVerified(), false, mockConfiguration.getDefaultResponse(), + mockConfiguration.useLastMatchResponseStrategy(), specification, this); if (method.getDeclaringClass() == ISpockMockObject.class) { return mockObject; diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/MockConfiguration.java b/spock-core/src/main/java/org/spockframework/mock/runtime/MockConfiguration.java index 76afefc530..445b72df3c 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/MockConfiguration.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/MockConfiguration.java @@ -31,6 +31,7 @@ public class MockConfiguration implements IMockConfiguration { private final List constructorArgs; private final List> additionalInterfaces; private final IDefaultResponse defaultResponse; + private final boolean useLastMatchResponseStrategy; private final boolean global; private final boolean verified; private final boolean useObjenesis; @@ -51,6 +52,7 @@ public MockConfiguration(@Nullable String name, Type type, @Nullable Object inst this.constructorArgs = getOptionAsList(options, "constructorArgs"); this.additionalInterfaces = getOption(options, "additionalInterfaces", List.class, Collections.emptyList()); this.defaultResponse = getOption(options, "defaultResponse", IDefaultResponse.class, this.nature.getDefaultResponse()); + this.useLastMatchResponseStrategy = getOption(options, "useLastMatchResponseStrategy", Boolean.class, false); this.global = getOption(options, "global", Boolean.class, false); this.verified = getOption(options, "verified", Boolean.class, this.nature.isVerified()); this.useObjenesis = getOption(options, "useObjenesis", Boolean.class, this.nature.isUseObjenesis()); @@ -103,6 +105,11 @@ public IDefaultResponse getDefaultResponse() { return defaultResponse; } + @Override + public boolean useLastMatchResponseStrategy() { + return useLastMatchResponseStrategy; + } + @Override public boolean isGlobal() { return global; 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 55dad1b4d0..c3514fb91b 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 @@ -30,18 +30,21 @@ public class MockObject implements IMockObject { private final boolean verified; private final boolean global; private final IDefaultResponse defaultResponse; + private final boolean useLastMatchResponseStrategy; private final SpecificationAttachable mockInterceptor; private Specification specification; public MockObject(@Nullable String name, Type type, Object instance, boolean verified, boolean global, - IDefaultResponse defaultResponse, Specification specification, SpecificationAttachable mockInterceptor) { + IDefaultResponse defaultResponse, boolean useLastMatchResponseStrategy, Specification specification, + SpecificationAttachable mockInterceptor) { this.name = name; this.type = type; this.instance = instance; this.verified = verified; this.global = global; this.defaultResponse = defaultResponse; + this.useLastMatchResponseStrategy = useLastMatchResponseStrategy; this.specification = specification; this.mockInterceptor = mockInterceptor; } @@ -77,6 +80,11 @@ public IDefaultResponse getDefaultResponse() { return defaultResponse; } + @Override + public boolean useLastMatchResponseStrategy() { + return useLastMatchResponseStrategy; + } + @Override public Specification getSpecification() { return specification; diff --git a/spock-core/src/main/java/spock/mock/MockingApi.java b/spock-core/src/main/java/spock/mock/MockingApi.java index b9a36ea3e5..1df8b8bd64 100644 --- a/spock-core/src/main/java/spock/mock/MockingApi.java +++ b/spock-core/src/main/java/spock/mock/MockingApi.java @@ -170,6 +170,7 @@ public T Mock( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -222,6 +223,7 @@ public T Mock( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -339,6 +341,7 @@ public T Mock( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -390,6 +393,7 @@ public T Stub( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -443,6 +447,7 @@ public T Stub( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -502,6 +507,7 @@ public T Stub( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -568,6 +574,7 @@ public T Stub( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -619,6 +626,7 @@ public T Spy( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -698,6 +706,7 @@ public T Spy( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -755,6 +764,8 @@ public T Spy( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -820,6 +831,7 @@ public T Spy( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class) }) @@ -871,6 +883,7 @@ public T GroovyMock( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -923,6 +936,7 @@ public T GroovyMock( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -983,6 +997,7 @@ public T GroovyMock( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -1050,6 +1065,7 @@ public T GroovyMock( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -1102,6 +1118,7 @@ public T GroovyStub( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -1154,6 +1171,7 @@ public T GroovyStub( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -1214,6 +1232,7 @@ public T GroovyStub( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -1281,6 +1300,7 @@ public T GroovyStub( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -1333,6 +1353,7 @@ public T GroovySpy( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -1385,6 +1406,7 @@ public T GroovySpy( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) @@ -1443,6 +1465,7 @@ public T GroovySpy( @NamedParam(value = "name", type = String.class), @NamedParam(value = "additionalInterfaces", type = List.class), @NamedParam(value = "defaultResponse", type = IDefaultResponse.class), + @NamedParam(value = "useLastMatchResponseStrategy", type = Boolean.class), @NamedParam(value = "verified", type = Boolean.class), @NamedParam(value = "useObjenesis", type = Boolean.class), @NamedParam(value = "global", type = Boolean.class) diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeMatching.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeMatching.groovy new file mode 100644 index 0000000000..70841438b9 --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeMatching.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2021 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 + * + * http://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 spock.lang.Specification + +/** + * + * @author Kamil Jędrzejuk + */ +class InteractionScopeMatching extends Specification { + List defaultMockBehaviour = Mock() + List withOverrideLastResponse = Mock([useLastMatchResponseStrategy:true]) + + def setup() { + defaultMockBehaviour.size() >> 1 + withOverrideLastResponse.size() >> 1 + } + + def "interactions should use response matching algorithm depends on passed useLastMatchResponseStrategy flag when determining the stubbed reply"() { + given: + defaultMockBehaviour.size() >> 2 + withOverrideLastResponse.size() >> 2 + + expect: + defaultMockBehaviour.size() == 1 + withOverrideLastResponse.size() == 2 + } +} 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 54d61be933..7122a71805 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 @@ -117,6 +117,11 @@ public IDefaultResponse getDefaultResponse() { return null; } + @Override + public boolean useLastMatchResponseStrategy() { + return false; + } + @Override public Specification getSpecification() { return null;