spock
spock copied to clipboard
Add support for chaining sequences of closures
This enables the use of closures in the lists passed to the triple-right-shift (>>>) operator.
Examples
- The expression
queue.poll() >> { throw new UnsupportedOperationException() } >> { throw new UnsupportedOperationException() } >> 0
can now be written as
queue.poll() >>> [{ throw new UnsupportedOperationException() }] * 2 >> 0
- The lists can be heterogeneous, containing both closures and values:
queue.poll() >>> [{ throw new UnsupportedOperationException() }, 0, { count++ }, 2, [3, 4]]
- More examples can be found in the test file spock-specs/src/test/groovy/org/spockframework/smoke/mock/ChainedResponseGenerators.groovy
Codecov Report
Merging #979 into master will decrease coverage by
<.01%
. The diff coverage is76.92%
.
@@ Coverage Diff @@
## master #979 +/- ##
============================================
- Coverage 75.99% 75.99% -0.01%
- Complexity 3544 3546 +2
============================================
Files 377 377
Lines 10788 10800 +12
Branches 1374 1377 +3
============================================
+ Hits 8198 8207 +9
- Misses 2109 2110 +1
- Partials 481 483 +2
Impacted Files | Coverage Δ | Complexity Δ | |
---|---|---|---|
...ework/mock/response/IterableResponseGenerator.java | 83.33% <76.92%> (-16.67%) |
7 <1> (+2) |
Continue to review full report at Codecov.
Legend - Click here to learn more
Δ = absolute <relative> (impact)
,ø = not affected
,? = missing data
Powered by Codecov. Last update ed38b97...1da3126. Read the comment docs.
Hi @jff, thanks for implementing this feature. When changing something on the spock-core language level, we have to ask ourselves, does this new feature improve the readability/clarity of the test without adding too much variation. Having different ways of accomplishing the same goal might be nicer, but it also increases the cognitive load on those who have to read an comprehend your tests.
This feature does not add something that couldn't be achieved before. Yes
queue.poll() >>> [{ throw new UnsupportedOperationException() }] * 2 >> 0
is a bit shorter than
queue.poll() >> { throw new UnsupportedOperationException() } >> { throw new UnsupportedOperationException() } >> 0
but now the reader has to calculate to see what happens. See DRY vs. DAMP.
I'm not totally against it, but it does seem to be for some edge cases. How often would you actually use this new feature?
Hi @leonard84, thanks for the quick and pertinent reply. Please find below a few additional points:
-
I would say that your comment on increasing the cognitive load could also be applied to the existing operator
>>>
. Why provide the operator>>>
and writesubscriber.receive(_) >>> ["ok", "error", "error", "ok"]
when one could simply do
subscriber.receive(_) >> "ok" >> "error" >> "error" >> "ok"
? -
One could see the operator
>>>
as a generalization of>>
. However, from a type-oriented perspective this generalization breaks down, because whilst>>
supports closures as operands,>>>
does not. I don't see a good reason to have this inconsistency. -
Regarding the DRY vs DAMP argument, I agree that duplication is more acceptable in tests than in software. However, in general, if we can avoid duplication without affecting the readability of the tests, I believe that we should avoid it. Spock's documentation mentions duplication as something to avoid. In fact, one of my favorite features in Spock, data tables, are an excellent mechanism to avoid duplication without hindering readability. My initial motivation to extend the operator
>>>
was to allow the use of sequences of closures in data tables.For example, I don't think it is far-fetched to have the need to test the internal state of a certain module that can throw a sequence of 3 or more exceptions (e.g. I am writing mock tests for a module that books "activities" involving hotels, cars, etc. and we are interested in testing certain sequences of exceptions).
If we are in a context where we want to test a certain state for the permutations of three possible exceptions, we would probably have to write 6 test methods, one for each sequence:
m() >> { throw new E1() } >> { throw new E2() } >> {thrown new E3()} ... when: x.process() then: x.getState() == STATE1 m() >> { throw new E1() } >> { throw new E3() } >> {thrown new E2()} ... when: x.process() then: x.getState() == STATE2 m() >> { throw new E2() } >> { throw new E1() } >> {thrown new E3()} ... when: x.process() then: x.getState() == STATE3 m() >> { throw new E2() } >> { throw new E3() } >> {thrown new E1()} ... when: x.process() then: x.getState() == STATE3 m() >> { throw new E3() } >> { throw new E1() } >> {thrown new E2()} ... when: x.process() then: x.getState() == STATE2 m() >> { throw new E3() } >> { throw new E2() } >> {thrown new E1()} ... when: x.process() then: x.getState() == STATE1
Using data tables and the new operator
>>>
, one could write a single test method:m() >>> sequence_exceptions when: x.process() then: x.getState() == state where: sequence_exceptions | state [{throw new E1()}, {throw new E2()}, {thrown new E3()}] | STATE1 [{throw new E1()}, {throw new E3()}, {thrown new E2()}] | STATE2 [{throw new E2()}, {throw new E1()}, {thrown new E3()}] | STATE3 [{throw new E2()}, {throw new E3()}, {thrown new E1()}] | STATE3 [{throw new E3()}, {throw new E1()}, {thrown new E2()}] | STATE2 [{throw new E3()}, {throw new E2()}, {thrown new E1()}] | STATE1
This is, in my view, more readable, mainly because I can see all the cases together without jumping from method to method.
Perhaps this feature will not be used in every project, but I believe that it improves the type consistency of
>>>
(when seen as a generalization of>>
). I also believe that there are projects that could benefit from having it, for it would improve the readability of tests that involve sequences of closures.
Another problem we have, is that using >>> [{ code }]
is currently the only easy way to actually return a closure. Which makes this a breaking change. As a workaround you could do >> {{ code }}
but that looks strange.