From d251246bd974003a5e2b1869fb342f380506780e Mon Sep 17 00:00:00 2001 From: Bouke Nijhuis Date: Sat, 26 Sep 2020 18:58:50 +0200 Subject: [PATCH 1/2] Added configurable interaction matching By default Spock uses a match first algorithm to determine the defined return value of a method. So whenever there are multiple defined return values, it will pick the first (that is not exhausted). This change enables a user to change this to a match last algorithm. So Spock will pick the last defined return value (that is not exhausted). This enables easy overriding of default return values. As requested in issue #26, #251, #321 and #962. --- .../mock/runtime/InteractionScope.java | 15 +++-- .../spock/config/RunnerConfiguration.java | 2 + .../mock/InteractionScopeMatching.groovy | 60 +++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeMatching.groovy 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..9c3e3cefe8 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 @@ -17,7 +17,9 @@ package org.spockframework.mock.runtime; import org.spockframework.mock.*; +import org.spockframework.runtime.RunContext; import org.spockframework.util.ExceptionUtil; +import spock.config.RunnerConfiguration; import java.util.*; @@ -33,6 +35,7 @@ public class InteractionScope implements IInteractionScope { private int currentRegistrationZone = 0; private int currentExecutionZone = 0; private final Deque previousInvocationsInReverseOrder = new ArrayDeque<>(MAX_PREVIOUS_INVOCATIONS); + private static final RunnerConfiguration configuration = RunContext.get().getConfiguration(RunnerConfiguration.class); @Override public void addInteraction(final IMockInteraction interaction) { @@ -83,14 +86,18 @@ public void addUnmatchedInvocation(IMockInvocation invocation) { @Override public IMockInteraction match(IMockInvocation invocation) { - IMockInteraction firstMatch = null; + IMockInteraction match = null; for (IMockInteraction interaction : interactions) if (interaction.matches(invocation)) { - if (!interaction.isExhausted()) return interaction; - if (firstMatch == null) firstMatch = interaction; + if (configuration.matchFirstInteraction) { + if (!interaction.isExhausted()) return interaction; + if (match == null) match = interaction; + } else { + if (!interaction.isExhausted() || match == null) match = interaction; + } } - return firstMatch; + return match; } @Override diff --git a/spock-core/src/main/java/spock/config/RunnerConfiguration.java b/spock-core/src/main/java/spock/config/RunnerConfiguration.java index 4d8292160b..383fb517cd 100644 --- a/spock-core/src/main/java/spock/config/RunnerConfiguration.java +++ b/spock-core/src/main/java/spock/config/RunnerConfiguration.java @@ -29,6 +29,7 @@ * baseClass IntegrationSpec * } * filterStackTrace true // this is the default + * matchFirstInteraction true // this is the default * } * */ @@ -38,4 +39,5 @@ public class RunnerConfiguration { public IncludeExcludeCriteria exclude = new IncludeExcludeCriteria(); public boolean filterStackTrace = true; public boolean optimizeRunOrder = false; + public boolean matchFirstInteraction = true; } 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..0c8fc0c2fe --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeMatching.groovy @@ -0,0 +1,60 @@ +/* + * Copyright 2020 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 org.spockframework.runtime.RunContext +import spock.config.RunnerConfiguration +import spock.lang.Specification + +/** + * + * @author Bouke Nijhuis + */ +class InteractionScopeMatching extends Specification { + List list = Mock() + + def setup() { + list.size() >> 1 + list.size() >> -1 + } + + def "RunnerConfiguration.matchFirstInteraction should default to true"() { + expect: + RunContext.get().getConfiguration(RunnerConfiguration.class).matchFirstInteraction + } + + def "interactions should (by default) use the first match algorithm when determining the stubbed reply"() { + expect: // it to use the first defined reply of the method + list.size() == 1 + } + + def "interactions should (when specified) use the last match algorithm when determining the stubbed reply"() { + RunContext.get().getConfiguration(RunnerConfiguration.class).matchFirstInteraction = false + + expect: // it to use the last defined reply of the method + list.size() == -1 + + when: // adding an interaction outside of the setup method + list.size() >> -2 + + then: // I expect it the use the last defined reply of the method + list.size() == -2 + + cleanup: + RunContext.get().getConfiguration(RunnerConfiguration.class).matchFirstInteraction = true + } +} From f591abb84f12003a2c7e171452c2593d47409897 Mon Sep 17 00:00:00 2001 From: Bouke Nijhuis Date: Sat, 3 Oct 2020 18:58:05 +0200 Subject: [PATCH 2/2] Improved test coverage --- .../mock/InteractionScopeExhaustion.groovy | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeExhaustion.groovy diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeExhaustion.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeExhaustion.groovy new file mode 100644 index 0000000000..f87bbbcafb --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/InteractionScopeExhaustion.groovy @@ -0,0 +1,113 @@ +/* + * Copyright 2020 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 org.spockframework.mock.IMockInteraction +import org.spockframework.mock.runtime.InteractionScope +import org.spockframework.mock.runtime.MockInvocation +import org.spockframework.runtime.RunContext +import spock.config.RunnerConfiguration +import spock.lang.Specification + +/** + * + * @author Bouke Nijhuis + */ +class InteractionScopeExhaustion extends Specification { + + def "exhausted interactions should be skipped (if possible) for the firstMatch algorithm"() { + expect: + test(match1, isExhausted1, match2, isExhausted2, resultText) + + where: + match1 | isExhausted1 || match2 | isExhausted2 || resultText + //false | false || false | false || + //false | false || false | true || + false | false || true | false || "2" + false | false || true | true || "2" + + //false | true || false | false || + //false | true || false | true || + false | true || true | false || "2" + false | true || true | true || "2" + + true | false || false | false || "1" + true | false || false | true || "1" + true | false || true | false || "1" + true | false || true | true || "1" + + true | true || false | false || "1" + true | true || false | true || "1" + true | true || true | false || "2" + true | true || true | true || "1" + } + + def "exhausted interactions should be skipped (if possible) for the lastMatch algorithm"() { + RunContext.get().getConfiguration(RunnerConfiguration.class).matchFirstInteraction = false + + expect: + test(match1, isExhausted1, match2, isExhausted2, resultText) + + cleanup: + RunContext.get().getConfiguration(RunnerConfiguration.class).matchFirstInteraction = true + + where: + match1 | isExhausted1 || match2 | isExhausted2 || resultText + //false | false || false | false || + //false | false || false | true || + false | false || true | false || "2" + false | false || true | true || "2" + + //false | true || false | false || + //false | true || false | true || + false | true || true | false || "2" + false | true || true | true || "2" + + true | false || false | false || "1" + true | false || false | true || "1" + true | false || true | false || "2" + true | false || true | true || "1" + + true | true || false | false || "1" + true | true || false | true || "1" + true | true || true | false || "2" + true | true || true | true || "1" + } + + + def test(boolean match1, boolean isExhausted1, boolean match2, boolean isExhausted2, String resultText) { + InteractionScope interactionScope = new InteractionScope() + MockInvocation mockInvocation = Mock() + + IMockInteraction mockInteraction1 = Mock() + interactionScope.addInteraction(mockInteraction1) + mockInteraction1.matches(mockInvocation) >> match1 + mockInteraction1.isExhausted() >> isExhausted1 + mockInteraction1.getText() >> "1" + + IMockInteraction mockInteraction2 = Mock() + interactionScope.addInteraction(mockInteraction2) + mockInteraction2.matches(mockInvocation) >> match2 + mockInteraction2.isExhausted() >> isExhausted2 + mockInteraction2.getText() >> "2" + + IMockInteraction mockInteractionResult = interactionScope.match(mockInvocation) + + return mockInteractionResult.getText() == resultText + } + +}