From 43996bf6a1816faf5abb9255cdd476301d296dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Thu, 23 Jul 2026 17:31:32 +0200 Subject: [PATCH 1/4] Support arrays in verifyEach Add verifyEach(U[], ...) overloads that delegate to the existing Iterable versions via Arrays.asList, so object arrays can be used directly without wrapping them in a list first. --- docs/release_notes.adoc | 1 + .../main/java/spock/lang/Specification.java | 54 ++++++++++++++++ .../smoke/VerifyEachBlocks.groovy | 64 +++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index 3241d6b7bc..d1d529c7c4 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -8,6 +8,7 @@ include::include.adoc[] === Enhancements +* Improve `verifyEach` to also accept object arrays, not only `Iterable` * Add support for `final` local variables in `where:` blocks, declared at their beginning and evaluated once per feature, scoped to the where-block spockIssue:138[] * Improve `TooManyInvocationsError` now reports unsatisfied interactions with argument mismatch details, making it easier to diagnose why invocations didn't match expected interactions spockPull:2315[] diff --git a/spock-core/src/main/java/spock/lang/Specification.java b/spock-core/src/main/java/spock/lang/Specification.java index b920228999..7ce050f9d5 100644 --- a/spock-core/src/main/java/spock/lang/Specification.java +++ b/spock-core/src/main/java/spock/lang/Specification.java @@ -24,6 +24,7 @@ import org.junit.platform.commons.annotation.Testable; import spock.mock.MockingApi; +import java.util.Arrays; import java.util.Objects; import java.util.function.Function; @@ -368,4 +369,57 @@ public void verifyEach( } SpockRuntime.verifyEach(things, namer, closure); } + + /** + * Performs assertions on each item, collecting up failures instead of stopping at first. + *

+ * Exception messages will contain a toString() of the item to identify it. + *

+ * The closure can either use one or two parameters. + * The first parameter will always be the item. + * The second optional parameter will be the iteration index of the item. + * + * @param things the array to inspect + * @param closure a code block containing top-level conditions + * @param type of items in things + * @since 2.5 + */ + @Beta + public void verifyEach( + U[] things, + @ClosureParams(value = FromString.class, options = {"U", "U, int"}) + @DelegatesTo(type = "U", strategy = Closure.DELEGATE_FIRST) + Closure closure + ) { + verifyEach(things, Objects::toString, closure); + } + + /** + * Performs assertions on each item, collecting up failures instead of stopping at first. + *

+ * Exception messages will contain the result of calling the namer for an item to identify it. + *

+ * The closure can either use one or two parameters. + * The first parameter will always be the item. + * The second optional parameter will be the iteration index of the item. + * + * @param things the array to inspect + * @param namer the namer function to use when rendering the exception + * @param closure a code block containing top-level conditions + * @param type of items in things + * @since 2.5 + */ + @Beta + public void verifyEach( + U[] things, + Function namer, + @ClosureParams(value = FromString.class, options = {"U", "U, int"}) + @DelegatesTo(type = "U", strategy = Closure.DELEGATE_FIRST) + Closure closure + ) { + if (things == null) { + throw new SpockAssertionError("Target of 'verifyEach' block must not be null"); + } + verifyEach(Arrays.asList(things), namer, closure); + } } diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy index 395b068b39..3055cd9aea 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy @@ -145,6 +145,70 @@ class VerifyEachBlocks extends EmbeddedSpecification { } } + def "verifyEach supports object arrays"() { + given: + Integer[] array = [1, 2, 3] + + expect: + verifyEach(array) { + it > 0 + } + + and: 'parameter is the same as the delegate' + verifyEach(array) { + it == delegate + } + } + + def "verifyEach on an array handles a failed element verification"() { + given: + Integer[] array = [1, 2, 3] + + when: + verifyEach(array) { + it < 3 + } + + then: + SpockAssertionError e = thrown() + e.message.contains('item[2] 3') + } + + def "verifyEach on an array supports the namer"() { + given: + Integer[] array = [1, 2, 3] + + when: + verifyEach(array, { "int($it)" }) { + it < 3 + } + + then: + SpockAssertionError e = thrown() + e.message.contains('int(3)') + } + + def "verifyEach on an array can have an optional index parameter"() { + given: + Integer[] array = [1, 2, 3] + + expect: + verifyEach(array) { it, index -> + it == index + 1 + } + } + + def "verifyEach on a null array fails with a clear message"() { + when: + verifyEach((Integer[]) null) { + it > 0 + } + + then: + SpockAssertionError e = thrown() + e.message == "Target of 'verifyEach' block must not be null" + } + void checks(int x) { doCheck(x, this.&nestedException) doCheck(x, this.&nestedAssertion) From 6ec5e6bcac6a68b7f907831073165cd4a0ab56c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Thu, 23 Jul 2026 17:42:53 +0200 Subject: [PATCH 2/4] Support maps in verifyEach Add verifyEach(Map, ...) overloads that iterate the map's entries. Following Groovy's Map.each idiom, the closure can use zero to three parameters: none (the entry is the delegate, exposing key and value), the entry, key and value, or key, value and index. --- docs/release_notes.adoc | 1 + docs/spock_primer.adoc | 14 ++++ .../spockframework/runtime/SpockRuntime.java | 40 +++++++++ .../main/java/spock/lang/Specification.java | 63 +++++++++++++++ .../docs/primer/VerifyEachDocSpec.groovy | 11 +++ .../smoke/VerifyEachBlocks.groovy | 81 +++++++++++++++++++ .../verifyEach_with_map_method.txt | 14 ++++ 7 files changed, 224 insertions(+) create mode 100644 spock-specs/src/test/resources/snapshots/org/spockframework/docs/primer/VerifyEachDocSpec/verifyEach_with_map_method.txt diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index d1d529c7c4..aec2bda032 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -9,6 +9,7 @@ include::include.adoc[] === Enhancements * Improve `verifyEach` to also accept object arrays, not only `Iterable` +* Improve `verifyEach` to accept a `Map`, iterating its entries with Groovy's key/value destructuring * Add support for `final` local variables in `where:` blocks, declared at their beginning and evaluated once per feature, scoped to the where-block spockIssue:138[] * Improve `TooManyInvocationsError` now reports unsatisfied interactions with argument mismatch details, making it easier to diagnose why invocations didn't match expected interactions spockPull:2315[] diff --git a/docs/spock_primer.adoc b/docs/spock_primer.adoc index 74d16d7e8a..e82913e21a 100644 --- a/docs/spock_primer.adoc +++ b/docs/spock_primer.adoc @@ -788,6 +788,20 @@ will be rendered as include::{snapshotdir}/primer/VerifyEachDocSpec/verifyEach_with_index_method.txt[] ---- +Since 2.5, `verifyEach` can also assert on the entries of a `Map`, mirroring Groovy's `Map.each` idiom. +The closure can use zero to three parameters: with no parameters the current `Map.Entry` is set as the delegate, so `key` and `value` can be referenced directly; a single parameter is the entry; two parameters are the key and the value; and a third optional parameter is the iteration index. + +[source,groovy,indent=0] +---- +include::{sourcedir}/primer/VerifyEachDocSpec.groovy[tag=verify-each-map] +---- + +will be rendered as + +---- +include::{snapshotdir}/primer/VerifyEachDocSpec/verifyEach_with_map_method.txt[] +---- + [[specifications-as-documentation]] == Specifications as Documentation diff --git a/spock-core/src/main/java/org/spockframework/runtime/SpockRuntime.java b/spock-core/src/main/java/org/spockframework/runtime/SpockRuntime.java index 61cda0712f..9714833373 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/SpockRuntime.java +++ b/spock-core/src/main/java/org/spockframework/runtime/SpockRuntime.java @@ -199,6 +199,46 @@ public static void verifyEach( } } + throwFailures(namer, failures); + } + + public static void verifyEach( + Map things, + Function, ?> namer, + @ClosureParams(value = FromString.class, options = {"", "java.util.Map.Entry", "K, V", "K, V, int"}) + @DelegatesTo(type = "java.util.Map.Entry", strategy = Closure.DELEGATE_FIRST) + Closure closure + ) { + List>> failures = new ArrayList<>(); + int index = -1; + // use plain loop here, so we don't generate an extra stack element on failure + for (Map.Entry entry : things.entrySet()) { + index++; + try { + closure.setDelegate(entry); // for conditions, so key and value can be used directly + closure.setResolveStrategy(Closure.DELEGATE_FIRST); + switch (closure.getMaximumNumberOfParameters()) { + case 0: + GroovyRuntimeUtil.invokeClosure(closure); + break; + case 1: + GroovyRuntimeUtil.invokeClosure(closure, entry); + break; + case 2: + GroovyRuntimeUtil.invokeClosure(closure, entry.getKey(), entry.getValue()); + break; + default: + GroovyRuntimeUtil.invokeClosure(closure, entry.getKey(), entry.getValue(), index); + } + } catch (Throwable throwable) { + failures.add(new InternalItemFailure<>(entry, index, throwable)); + } + } + + throwFailures(namer, failures); + } + + private static void throwFailures(Function namer, List> failures) { if (failures.size() == 1) { throw getAssertionFailedError(namer, failures.get(0)); } else if (!failures.isEmpty()) { diff --git a/spock-core/src/main/java/spock/lang/Specification.java b/spock-core/src/main/java/spock/lang/Specification.java index 7ce050f9d5..522cac1961 100644 --- a/spock-core/src/main/java/spock/lang/Specification.java +++ b/spock-core/src/main/java/spock/lang/Specification.java @@ -25,6 +25,7 @@ import spock.mock.MockingApi; import java.util.Arrays; +import java.util.Map; import java.util.Objects; import java.util.function.Function; @@ -422,4 +423,66 @@ public void verifyEach( } verifyEach(Arrays.asList(things), namer, closure); } + + /** + * Performs assertions on each entry of a map, collecting up failures instead of stopping at first. + *

+ * Exception messages will contain a toString() of the entry ({@code key=value}) to identify it. + *

+ * The closure can use zero to three parameters, mirroring Groovy's {@code Map.each} idiom. + * With no parameters, the delegate is the current {@link java.util.Map.Entry}, so {@code key} + * and {@code value} can be referenced directly. + * A single parameter is the entry, two parameters are the key and the value, and a third + * optional parameter is the iteration index of the entry. + * + * @param things the map to inspect + * @param closure a code block containing top-level conditions + * @param type of keys in things + * @param type of values in things + * @since 2.5 + */ + @Beta + public void verifyEach( + Map things, + @ClosureParams(value = FromString.class, options = {"", "java.util.Map.Entry", "K, V", "K, V, int"}) + @DelegatesTo(type = "java.util.Map.Entry", strategy = Closure.DELEGATE_FIRST) + Closure closure + ) { + verifyEach(things, Objects::toString, closure); + } + + /** + * Performs assertions on each entry of a map, collecting up failures instead of stopping at first. + *

+ * Exception messages will contain the result of calling the namer for an entry to identify it. + *

+ * The closure can use zero to three parameters, mirroring Groovy's {@code Map.each} idiom. + * With no parameters, the delegate is the current {@link java.util.Map.Entry}, so {@code key} + * and {@code value} can be referenced directly. + * A single parameter is the entry, two parameters are the key and the value, and a third + * optional parameter is the iteration index of the entry. + * + * @param things the map to inspect + * @param namer the namer function to use when rendering the exception + * @param closure a code block containing top-level conditions + * @param type of keys in things + * @param type of values in things + * @since 2.5 + */ + @Beta + public void verifyEach( + Map things, + Function, ?> namer, + @ClosureParams(value = FromString.class, options = {"", "java.util.Map.Entry", "K, V", "K, V, int"}) + @DelegatesTo(type = "java.util.Map.Entry", strategy = Closure.DELEGATE_FIRST) + Closure closure + ) { + if (things == null) { + throw new SpockAssertionError("Target of 'verifyEach' block must not be null"); + } + if (namer == null) { + throw new SpockAssertionError("Namer for a 'verifyEach' block must not be null"); + } + SpockRuntime.verifyEach(things, namer, closure); + } } diff --git a/spock-specs/src/test/groovy/org/spockframework/docs/primer/VerifyEachDocSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/docs/primer/VerifyEachDocSpec.groovy index a2a0d7f77a..3700ab59b0 100644 --- a/spock-specs/src/test/groovy/org/spockframework/docs/primer/VerifyEachDocSpec.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/docs/primer/VerifyEachDocSpec.groovy @@ -108,4 +108,15 @@ list.every { it == 2 } MultipleFailuresError e = thrown() snap.assertThat(e.message).matchesSnapshot() } + + def "verifyEach with map method"(@Snapshot Snapshotter snap) { + when: + // tag::verify-each-map[] + def map = [a: 1, b: 2, c: 3] + verifyEach(map) { key, value -> value == 2 } + // end::verify-each-map[] + then: + MultipleFailuresError e = thrown() + snap.assertThat(e.message).matchesSnapshot() + } } diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy index 3055cd9aea..ac2e8c41c8 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy @@ -209,6 +209,87 @@ class VerifyEachBlocks extends EmbeddedSpecification { e.message == "Target of 'verifyEach' block must not be null" } + def "verifyEach supports maps with a key and value parameter"() { + given: + def map = [a: 1, b: 2, c: 3] + + expect: + verifyEach(map) { key, value -> + key instanceof String + value > 0 + } + } + + def "verifyEach supports maps with a single entry parameter"() { + given: + def map = [a: 1, b: 2, c: 3] + + expect: + verifyEach(map) { entry -> + entry.value > 0 + } + } + + def "verifyEach on a map delegates to the entry so key and value can be used directly"() { + given: + def map = [a: 1, b: 2, c: 3] + + expect: + verifyEach(map) { + value > 0 + key instanceof String + } + } + + def "verifyEach on a map can have an optional index parameter"() { + given: + def map = [a: 1, b: 2, c: 3] + + expect: + verifyEach(map) { key, value, index -> + value == index + 1 + } + } + + def "verifyEach on a map handles a failed element verification"() { + given: + def map = [a: 1, b: 2, c: 3] + + when: + verifyEach(map) { key, value -> + value < 3 + } + + then: + SpockAssertionError e = thrown() + e.message.contains('item[2] c=3') + } + + def "verifyEach on a map supports the namer"() { + given: + def map = [a: 1, b: 2, c: 3] + + when: + verifyEach(map, { "entry($it.key)" }) { key, value -> + value < 3 + } + + then: + SpockAssertionError e = thrown() + e.message.contains('entry(c)') + } + + def "verifyEach on a null map fails with a clear message"() { + when: + verifyEach((Map) null) { + value > 0 + } + + then: + SpockAssertionError e = thrown() + e.message == "Target of 'verifyEach' block must not be null" + } + void checks(int x) { doCheck(x, this.&nestedException) doCheck(x, this.&nestedAssertion) diff --git a/spock-specs/src/test/resources/snapshots/org/spockframework/docs/primer/VerifyEachDocSpec/verifyEach_with_map_method.txt b/spock-specs/src/test/resources/snapshots/org/spockframework/docs/primer/VerifyEachDocSpec/verifyEach_with_map_method.txt new file mode 100644 index 0000000000..b934a832fd --- /dev/null +++ b/spock-specs/src/test/resources/snapshots/org/spockframework/docs/primer/VerifyEachDocSpec/verifyEach_with_map_method.txt @@ -0,0 +1,14 @@ +Multiple Failures (2 failures) + org.spockframework.runtime.SpockAssertionError: Assertions failed for item[0] a=1: +Condition not satisfied: + +value == 2 +| | +1 false + + org.spockframework.runtime.SpockAssertionError: Assertions failed for item[2] c=3: +Condition not satisfied: + +value == 2 +| | +3 false From 514f39d45cd9e322b2eafef0734a88e68d636588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Thu, 23 Jul 2026 17:55:18 +0200 Subject: [PATCH 3/4] Validate verifyEach closure parameter count up front Reject an unsupported number of closure parameters (0 to 2 for iterables and arrays, 0 to 3 for maps) with a clear InvalidSpecException before iterating, instead of collecting a confusing per-item failure for each element. A zero-parameter closure asserts against the delegate (the item, or the map entry exposing key and value). --- docs/release_notes.adoc | 1 + .../spockframework/runtime/SpockRuntime.java | 33 ++++++++++--- .../smoke/VerifyEachBlocks.groovy | 48 +++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index aec2bda032..1f2be1b97d 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -26,6 +26,7 @@ include::include.adoc[] === Breaking Changes +* `verifyEach` now validates the closure parameter count up front and throws an `InvalidSpecException` for an unsupported number of parameters (0 to 2 for iterables and arrays), instead of failing with a less clear error while iterating * Mock/Stub checks on `Comparable` with `T` being something other than `Object` now compare using the java identity hash code instead of always being equal spockIssue:2352[] == 2.4 (2025-12-11) diff --git a/spock-core/src/main/java/org/spockframework/runtime/SpockRuntime.java b/spock-core/src/main/java/org/spockframework/runtime/SpockRuntime.java index 9714833373..0b752255fd 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/SpockRuntime.java +++ b/spock-core/src/main/java/org/spockframework/runtime/SpockRuntime.java @@ -181,18 +181,31 @@ public static void verifyEach( @DelegatesTo(type = "T", strategy = Closure.DELEGATE_FIRST) Closure closure ) { + int parameterCount = closure.getMaximumNumberOfParameters(); + // validate up front so the error is reported once instead of being collected per item + if (parameterCount > 2) { + throw new InvalidSpecException( + "verifyEach supports closures with 0 to 2 parameters (the item, optionally followed by the index), but got %d") + .withArgs(parameterCount); + } + List> failures = new ArrayList<>(); int index = -1; // use plain loop here, so we don't generate an extra stack element on failure for (T thing : things) { index++; try { - closure.setDelegate(thing); // for conditions + closure.setDelegate(thing); // for conditions, so the item can be used directly closure.setResolveStrategy(Closure.DELEGATE_FIRST); - if (closure.getMaximumNumberOfParameters() == 1) { - GroovyRuntimeUtil.invokeClosure(closure, thing); - } else { - GroovyRuntimeUtil.invokeClosure(closure, thing, index); + switch (parameterCount) { + case 0: + GroovyRuntimeUtil.invokeClosure(closure); + break; + case 1: + GroovyRuntimeUtil.invokeClosure(closure, thing); + break; + default: + GroovyRuntimeUtil.invokeClosure(closure, thing, index); } } catch (Throwable throwable) { failures.add(new InternalItemFailure<>(thing, index, throwable)); @@ -209,6 +222,14 @@ public static void verifyEach( @DelegatesTo(type = "java.util.Map.Entry", strategy = Closure.DELEGATE_FIRST) Closure closure ) { + int parameterCount = closure.getMaximumNumberOfParameters(); + // validate up front so the error is reported once instead of being collected per entry + if (parameterCount > 3) { + throw new InvalidSpecException( + "verifyEach on a map supports closures with 0 to 3 parameters (the entry, or the key and value, optionally followed by the index), but got %d") + .withArgs(parameterCount); + } + List>> failures = new ArrayList<>(); int index = -1; // use plain loop here, so we don't generate an extra stack element on failure @@ -217,7 +238,7 @@ public static void verifyEach( try { closure.setDelegate(entry); // for conditions, so key and value can be used directly closure.setResolveStrategy(Closure.DELEGATE_FIRST); - switch (closure.getMaximumNumberOfParameters()) { + switch (parameterCount) { case 0: GroovyRuntimeUtil.invokeClosure(closure); break; diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy index ac2e8c41c8..848b762bc1 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy @@ -18,6 +18,7 @@ package org.spockframework.smoke import org.opentest4j.MultipleFailuresError import org.spockframework.EmbeddedSpecification +import org.spockframework.runtime.InvalidSpecException import org.spockframework.runtime.SpockAssertionError import spock.lang.Snapshot import spock.lang.Snapshotter @@ -290,6 +291,53 @@ class VerifyEachBlocks extends EmbeddedSpecification { e.message == "Target of 'verifyEach' block must not be null" } + def "verifyEach on a map supports a closure with no parameters using the delegate"() { + given: + def map = [a: 1, b: 2, c: 3] + + expect: + verifyEach(map) { -> + value > 0 + } + } + + def "verifyEach rejects a closure with too many parameters for an iterable"() { + when: + verifyEach([1, 2, 3]) { a, b, c -> } + + then: + InvalidSpecException e = thrown() + e.message == "verifyEach supports closures with 0 to 2 parameters (the item, optionally followed by the index), but got 3" + } + + def "verifyEach on an iterable supports a closure with no parameters using the delegate"() { + given: + def list = ['abc', 'defg'] + + expect: + verifyEach(list) { -> + length() > 2 + } + } + + def "verifyEach rejects a closure with too many parameters for an array"() { + when: + verifyEach([1, 2, 3] as Integer[]) { a, b, c -> } + + then: + InvalidSpecException e = thrown() + e.message == "verifyEach supports closures with 0 to 2 parameters (the item, optionally followed by the index), but got 3" + } + + def "verifyEach rejects a closure with too many parameters for a map"() { + when: + verifyEach([a: 1, b: 2, c: 3]) { a, b, c, d -> } + + then: + InvalidSpecException e = thrown() + e.message == "verifyEach on a map supports closures with 0 to 3 parameters (the entry, or the key and value, optionally followed by the index), but got 4" + } + void checks(int x) { doCheck(x, this.&nestedException) doCheck(x, this.&nestedAssertion) From a7ef128999a9a7b7cd88571b735a66f911c687af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Fri, 24 Jul 2026 18:18:41 +0200 Subject: [PATCH 4/4] Support streams in verifyEach Add verifyEach overloads accepting java.util.stream.Stream (with and without a namer), adapting the single-use stream to an Iterable that is iterated exactly once. --- docs/release_notes.adoc | 7 ++- .../main/java/spock/lang/Specification.java | 55 +++++++++++++++++ .../smoke/VerifyEachBlocks.groovy | 59 +++++++++++++++++++ 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index 1f2be1b97d..9e79bbde71 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -8,8 +8,9 @@ include::include.adoc[] === Enhancements -* Improve `verifyEach` to also accept object arrays, not only `Iterable` -* Improve `verifyEach` to accept a `Map`, iterating its entries with Groovy's key/value destructuring +* Improve `verifyEach` to also accept object arrays, not only `Iterable` spockPull:2398[] +* Improve `verifyEach` to also accept a `java.util.stream.Stream` spockPull:2398[] +* Improve `verifyEach` to accept a `Map`, iterating its entries with Groovy's key/value destructuring spockPull:2398[] * Add support for `final` local variables in `where:` blocks, declared at their beginning and evaluated once per feature, scoped to the where-block spockIssue:138[] * Improve `TooManyInvocationsError` now reports unsatisfied interactions with argument mismatch details, making it easier to diagnose why invocations didn't match expected interactions spockPull:2315[] @@ -26,7 +27,7 @@ include::include.adoc[] === Breaking Changes -* `verifyEach` now validates the closure parameter count up front and throws an `InvalidSpecException` for an unsupported number of parameters (0 to 2 for iterables and arrays), instead of failing with a less clear error while iterating +* `verifyEach` now validates the closure parameter count up front and throws an `InvalidSpecException` when the closure declares more parameters than supported (up to 2 for iterables, arrays, and streams; up to 3 for maps), instead of failing with a less clear error while iterating spockPull:2398[] * Mock/Stub checks on `Comparable` with `T` being something other than `Object` now compare using the java identity hash code instead of always being equal spockIssue:2352[] == 2.4 (2025-12-11) diff --git a/spock-core/src/main/java/spock/lang/Specification.java b/spock-core/src/main/java/spock/lang/Specification.java index 522cac1961..82a5d8bafd 100644 --- a/spock-core/src/main/java/spock/lang/Specification.java +++ b/spock-core/src/main/java/spock/lang/Specification.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.Function; +import java.util.stream.Stream; import org.spockframework.lang.ISpecificationContext; import org.spockframework.lang.Wildcard; @@ -424,6 +425,60 @@ public void verifyEach( verifyEach(Arrays.asList(things), namer, closure); } + /** + * Performs assertions on each element of a stream, collecting up failures instead of stopping at first. + *

+ * Exception messages will contain a toString() of the element to identify it. + *

+ * The closure can either use one or two parameters. + * The first parameter will always be the element. + * The second optional parameter will be the iteration index of the element. + * + * @param things the stream to inspect + * @param closure a code block containing top-level conditions + * @param type of elements in things + * @since 2.5 + */ + @Beta + public void verifyEach( + Stream things, + @ClosureParams(value = FromString.class, options = {"U", "U, int"}) + @DelegatesTo(type = "U", strategy = Closure.DELEGATE_FIRST) + Closure closure + ) { + verifyEach(things, Objects::toString, closure); + } + + /** + * Performs assertions on each element of a stream, collecting up failures instead of stopping at first. + *

+ * Exception messages will contain the result of calling the namer for an element to identify it. + *

+ * The closure can either use one or two parameters. + * The first parameter will always be the element. + * The second optional parameter will be the iteration index of the element. + * + * @param things the stream to inspect + * @param namer the namer function to use when rendering the exception + * @param closure a code block containing top-level conditions + * @param type of elements in things + * @since 2.5 + */ + @Beta + public void verifyEach( + Stream things, + Function namer, + @ClosureParams(value = FromString.class, options = {"U", "U, int"}) + @DelegatesTo(type = "U", strategy = Closure.DELEGATE_FIRST) + Closure closure + ) { + if (things == null) { + throw new SpockAssertionError("Target of 'verifyEach' block must not be null"); + } + // a Stream is single-use, so adapt it to an Iterable that iterates it exactly once + verifyEach((Iterable) things::iterator, namer, closure); + } + /** * Performs assertions on each entry of a map, collecting up failures instead of stopping at first. *

diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy index 848b762bc1..e374381548 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/VerifyEachBlocks.groovy @@ -210,6 +210,65 @@ class VerifyEachBlocks extends EmbeddedSpecification { e.message == "Target of 'verifyEach' block must not be null" } + def "verifyEach supports streams"() { + given: + def stream = [1, 2, 3].stream() + + expect: + verifyEach(stream) { + it > 0 + } + } + + def "verifyEach on a stream handles a failed element verification"() { + given: + def stream = [1, 2, 3].stream() + + when: + verifyEach(stream) { + it < 3 + } + + then: + SpockAssertionError e = thrown() + e.message.contains('item[2] 3') + } + + def "verifyEach on a stream supports the namer"() { + given: + def stream = [1, 2, 3].stream() + + when: + verifyEach(stream, { "int($it)" }) { + it < 3 + } + + then: + SpockAssertionError e = thrown() + e.message.contains('int(3)') + } + + def "verifyEach on a stream can have an optional index parameter"() { + given: + def stream = [1, 2, 3].stream() + + expect: + verifyEach(stream) { it, index -> + it == index + 1 + } + } + + def "verifyEach on a null stream fails with a clear message"() { + when: + verifyEach((java.util.stream.Stream) null) { + it > 0 + } + + then: + SpockAssertionError e = thrown() + e.message == "Target of 'verifyEach' block must not be null" + } + def "verifyEach supports maps with a key and value parameter"() { given: def map = [a: 1, b: 2, c: 3]