-
Notifications
You must be signed in to change notification settings - Fork 470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added configurable interaction matching #1219
base: master
Are you sure you want to change the base?
Added configurable interaction matching #1219
Conversation
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 spockframework#26, spockframework#251, spockframework#321 and spockframework#962.
Codecov Report
@@ Coverage Diff @@
## master #1219 +/- ##
============================================
+ Coverage 76.28% 76.30% +0.02%
- Complexity 3678 3684 +6
============================================
Files 396 396
Lines 11265 11269 +4
Branches 1382 1384 +2
============================================
+ Hits 8593 8599 +6
+ Misses 2193 2191 -2
Partials 479 479
Continue to review full report at Codecov.
|
Hi, thanks for the initiative, but I don't think that this is the right approach. |
Thanks for your reply. The global effect is intended. I think it would be really confusing if it would be tied to a specification (context). 😊 Regarding the problem discussed in #321. What I read there is that people really would like to override default mock responses in another place then the then: block. Currently if you do this, it is just ignored. I think lots of people would like the ability to override everywhere (especially in the given: or when: block). So the PR tries to enable overriding everywhere. In #321 you show that one cannot override '1 * get(0) >> 1' with '1 * get(0) >> 2'. In my opinion nobody is asking for this. People just want to be able to override mock response. In the mentioned issue they respectfully care less about the '1 *' part. They mostly care about the '>> 2' part. This is how I interpreted the request/problem. So I think we disagree about two thing:
Let's start with the first. Is my interpretation of the request in the issues correct (the ability to override mock responses everywhere)? If not, what do you think the interpretation should be? |
First, this is how spock defines the terms:
Those can be combined Now to the implications of this change, what you want is that a later defined stubbing would be used instead of the one defined before. The problem I see is that this also affects combined mocking/stubbing, like in this test. This would break if we set given:
def list = Mock(List)
when:
def result = list.get(42)
def result2 = list.get(42)
then:
1 * list.get(42) >> "foo"
1 * list.get(42) >> "bar"
and:
result == "foo"
result2 == "bar" |
Thanks for your elaboration and sorry for using the wrong terms. I will try to use the correct ones in the future. The example that you provide (with matchFirstInteraction = false) should fail. It will match the last interaction first (so the one that returns "bar"). If one would switch the statements in the when: or then: block the test will succeed. For me this is logical, because we use the match last algorithm. Do you expect your example test to succeed? If yes, why? |
It was not intended as a rebuke, just to clarify the terms to make communicating easier. The goal of #321 is that you can override the stubbing, the way this PR achieves that, has side effects that also affect normal mocking. As the inverted interaction matching is not obvious when looking at the test I think this violates the principle of least surprise. For someone looking at the test it doesn't do what you expect it to do. |
Is it a bad thing that it also affects mocking? I like the consistency in that. Regarding the principle of least surprise, I see your point there. What would be a better way to implement it? |
We could open a new interaction scope at the start of the method, this way you could define some stubbing in the The most reliable way would be to just extend the docs with this, and it would also be consistent with how mocks work. class OverrideStubbing extends Specification{
List stub = Stub() {
get(_) >> 1
}
def "normal"() {
expect:
stub.get(0) == 1
}
def "override"() {
when:
def result = stub.get(0)
then:
stub.get(_) >> 42
result == 42
}
}
|
Your proposal to extend the docs is a solution, but it does not solve the request to be able to override everywhere. So this solution is not the solution I am looking for (or the people in the mentioned issues). I am willing to spend time to implement the override everywhere, but then we have to find a solution that is acceptable for you. So how would you like to implement this? |
Could maybe the interaction block be extended for doing it explicitly on a case by case basis? Something like interaction(priority: 5) {
1 * subscriber.receive(message)
} or interaction(overwrite: true) {
1 * subscriber.receive(message)
} or something like that? |
@BoukeNijhuis could you give more examples of where you'd actually like to override this, that couldn't be solved by using the The main challenge to a solution IMO is that Spock doesn't add the interactions to a mock, so you can't just reset the mock like you can do in mockito. Instead the interactions are added to the Specifications To illustrate class AClass extends Specification {
List stub = Stub() {
get(_) >> 1 //1
}
def "atest"() {
given:
stub.get(_) >> 2 //2
when:
def result = stub.get(42)
then:
result == 3
_.get(_) >> 3 //3
}
} produces @org.spockframework.runtime.model.SpecMetadata(filename = 'script1603882731842.groovy', line = 3)
public class AClass extends spock.lang.Specification {
@org.spockframework.runtime.model.FieldMetadata(name = 'stub', ordinal = 0, line = 4, initializer = true)
private java.util.List stub
private java.lang.Object $spock_initializeFields() {
stub = this.StubImpl('stub', java.util.List, {
this.getSpecificationContext().getMockController().addInteraction(new org.spockframework.mock.runtime.InteractionBuilder(5, 4, 'get(_) >> 1').addEqualTarget(it).addEqualMethodName('get').setArgListKind(true, false).addEqualArg(_).addConstantResponse(1).build())
})
}
@org.spockframework.runtime.model.FeatureMetadata(name = 'atest', ordinal = 0, line = 8, blocks = [org.spockframework.runtime.model.BlockKind.SETUP[]org.codehaus.groovy.ast.AnnotationNode@d9f41, org.spockframework.runtime.model.BlockKind.WHEN[]org.codehaus.groovy.ast.AnnotationNode@d9f41, org.spockframework.runtime.model.BlockKind.THEN[]org.codehaus.groovy.ast.AnnotationNode@d9f41], parameterNames = [])
public void $spock_feature_0_0() {
org.spockframework.runtime.ErrorCollector $spock_errorCollector = new org.spockframework.runtime.ErrorCollector(false)
org.spockframework.runtime.ValueRecorder $spock_valueRecorder = new org.spockframework.runtime.ValueRecorder()
try {
this.getSpecificationContext().getMockController().addInteraction(new org.spockframework.mock.runtime.InteractionBuilder(10, 5, 'stub.get(_) >> 2').addEqualTarget(stub).addEqualMethodName('get').setArgListKind(true, false).addEqualArg(_).addConstantResponse(2).build())
this.getSpecificationContext().getMockController().enterScope()
this.getSpecificationContext().getMockController().addInteraction(new org.spockframework.mock.runtime.InteractionBuilder(17, 5, '_.get(_) >> 3').addEqualTarget(_).addEqualMethodName('get').setArgListKind(true, false).addEqualArg(_).addConstantResponse(3).build())
java.lang.Object result = stub.get(42)
this.getSpecificationContext().getMockController().leaveScope()
try {
org.spockframework.runtime.SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), 'result == 3', 16, 5, null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(2), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(0), result) == $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(1), 3)))
}
catch (java.lang.Throwable throwable) {
org.spockframework.runtime.SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, 'result == 3', 16, 5, null, throwable)}
finally {
}
this.getSpecificationContext().getMockController().leaveScope()
}
finally {
$spock_errorCollector.validateCollectedErrors()}
}
} One Scope is implicitly created at the start of the feature, the interactions 1 and 2 are added to it, then the |
Personally I would like to override in the setup and when block. This is where I specify my stub responses. In the then block I verify the actual outcome with the expected outcome. Therefore it is unnatural for me to override a stub response in the then block. To answer your question: I think everything can be solved by overriding in the then block. But I prefer to do it somewhere else. Then regarding the scopes and adding of interactions. I think I understand the current mechanism. I propose another mechanism that is more in line with Mockito's style. This new mechanism would enable the override everywhere request. The new mechanism would not add to a list of interactions, but it would just have one interaction which is overridable. By using the new mechanism you lose the ability to use example 3 (wildcard/regex targets). The current mechanism would be the default mechanism, but a user can choose to use the proposed mechanism. |
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.
This is a first attempt and will need some work. First I would like to know if you (the maintainers) are interested is this kind of configurable behaviour for interaction matching. If so, I think we should at least discuss the following topics: