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 3b2b0b96a2..f9b9152d96 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 a484ca3300..0b9d4ac277 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 @@ -116,7 +116,8 @@ 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); + 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 99e7224968..daf60536a0 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 @@ -83,6 +83,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)) { @@ -93,6 +101,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 af2d042e44..2381ffe78a 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 d20fdddcb1..ba0f66e7e0 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) }) @@ -390,6 +392,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 +446,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 +506,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 +573,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 +625,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) }) @@ -726,6 +733,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) }) @@ -783,6 +791,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) }) @@ -848,6 +857,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) }) @@ -899,6 +909,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) @@ -951,6 +962,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) @@ -1011,6 +1023,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) @@ -1078,6 +1091,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) @@ -1130,6 +1144,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) @@ -1182,6 +1197,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) @@ -1242,6 +1258,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) @@ -1309,6 +1326,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) @@ -1361,6 +1379,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) @@ -1413,6 +1432,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) @@ -1471,6 +1491,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) @@ -1537,6 +1558,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..07f8800d0b --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeMatching.groovy @@ -0,0 +1,46 @@ +/* + * 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 + + and: + withOverrideLastResponse.size() >> 3 + + expect: + defaultMockBehaviour.size() == 1 + withOverrideLastResponse.size() == 3 + } +} 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 9cae949d2f..727c9974c0 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 @@ -116,6 +116,11 @@ public IDefaultResponse getDefaultResponse() { return null; } + @Override + public boolean useLastMatchResponseStrategy() { + return false; + } + @Override public Specification getSpecification() { return null;