Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
88fbb2a
Detect and validate leading final where-block variables (#138)
leonard84 Jun 6, 2026
f6d19c3
Evaluate where-block variables once and pass them to data providers (…
leonard84 Jun 6, 2026
1923a19
Pass where-block variables to the data processor for derived variable…
leonard84 Jun 6, 2026
7931bda
Guard where-block variable name collisions and lock in scoping (#138)
leonard84 Jun 6, 2026
f8fa4c7
Document where-block local variables (#138)
leonard84 Jun 6, 2026
33ad1b4
Verify where-block variables with combined providers; fix StoreSpec f…
leonard84 Jun 6, 2026
be0905e
Add AST snapshot test for where-block variables (#138)
leonard84 Jun 6, 2026
259d651
Add comprehensive AST snapshot combining where-block variables with t…
leonard84 Jun 6, 2026
f9e58da
Reject user variable names using the reserved $spock_ prefix (#138)
leonard84 Jun 6, 2026
de193fe
Improve test
leonard84 Jun 6, 2026
aae8a42
Allow filter-blocks to reference where-block variables (#138)
leonard84 Jun 6, 2026
1d5dcf3
Support multiple-assignment where-block variables (#138)
leonard84 Jun 7, 2026
101daff
Auto-close AutoCloseable where-block variables (#138)
leonard84 Jun 7, 2026
df34b4d
Address review feedback for where-block variables (#138)
leonard84 Jun 7, 2026
30b408d
Require a data variable when where-block variables are declared (#138)
leonard84 Jun 10, 2026
806a30b
Fix parallel-execution race in WhereBlockVariables close assertions (…
leonard84 Jun 11, 2026
a74e133
Address second-round review feedback for where-block variables (#138)
leonard84 Jun 12, 2026
4766fef
Close already-created where-block variables when an initializer fails…
leonard84 Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/data_driven_testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,47 @@ This also includes columns in previous data tables in the same `where` block:
include::{sourcedir}/datadriven/DataSpec.groovy[tag=previous-column-access-multi-table]
----

== Local Variables in the where-block

Sometimes you need a helper value to build your data, but it should be neither a data variable (and thus a method parameter) nor a field on the whole specification.
You can declare a `final` local variable at the *start* of the `where:` block:

[source,groovy,indent=0]
----
...
Comment thread
AndreasTu marked this conversation as resolved.
include::{sourcedir}/datadriven/DataSpec.groovy[tag=where-block-variable]
----

Such variables:

* must be declared `final` (a bare `x = ...` declares a <<Data Variable Assignment,derived data variable>> instead);
* must come *before* any data variable, data table or data pipe;
* require at least one data variable in the same `where:` block, otherwise they would never be evaluated;
* are evaluated *once per feature* and reused everywhere they are referenced in the `where:` block, so `final fixture = new Fixture()` is a single shared instance;
* are visible only inside the `where:` block (data tables, data pipes, derived data variables and the `filter:` block); they are *not* method parameters and are *not* visible in the rest of the feature method;
* follow the same access rules as data providers: they may read `@Shared` and `static` fields but not plain instance fields;
* are automatically closed after the feature has finished if they implement `AutoCloseable`, in reverse declaration order; any error thrown while closing is ignored.

NOTE: In short, a where-block variable behaves like a feature-local `@Shared @AutoCleanup(quiet = true)` field: it is created once, shared across all iterations of the feature, and closed when the feature finishes.

The automatic close also covers a failed setup: if one initializer throws after earlier variables were already created, the already-created resources are closed in reverse declaration order, and any error thrown while closing them is attached to the original failure as a suppressed exception.
Only values that never made it into a variable are out of reach, for example a resource created inside the failing initializer expression itself.

The automatic close is best-effort and has one known limitation.
If a where-block variable is also consumed directly as a data provider (for example `x << resource` where `resource` is a where-block variable), it can be closed twice, so give such a value an idempotent `close()` if double-closing would be a problem.

The initializer may also use Groovy's multiple-assignment syntax to declare several locals at once.
The same rules apply to every target, and each name becomes its own where-block variable:

[source,groovy,indent=0]
----
...
Comment thread
AndreasTu marked this conversation as resolved.
include::{sourcedir}/datadriven/DataSpec.groovy[tag=where-block-variable-multi-assignment]
----

NOTE: The `final (a, b) = ...` syntax is only supported on Groovy 3.0 or newer.
On Groovy 2.5 declare one `final` local per line instead.

== Multi-Variable Assignment

Like with data pipes, you can also assign to multiple variables in one expression, if you have some object Groovy
Expand Down
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ include::include.adoc[]

=== Enhancements

* 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[]

=== Misc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ public class AstNodeCache {
public final MethodNode SpockRuntime_CallBlockExited =
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.CALL_BLOCK_EXITED).get(0);

public final MethodNode SpockRuntime_CloseWhereBlockVariablesAfterFailure =
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.CLOSE_WHERE_BLOCK_VARIABLES_AFTER_FAILURE).get(0);

public final MethodNode ValueRecorder_Reset =
ValueRecorder.getDeclaredMethods(org.spockframework.runtime.ValueRecorder.RESET).get(0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ public static void setFinal(FieldNode field, boolean isFinal) {
field.setModifiers(modifiers);
}

public static boolean isFinal(VariableExpression varExpr) {
return (varExpr.getModifiers() & Opcodes.ACC_FINAL) != 0;
}

public static boolean isJointCompiled(ClassNode clazz) {
return clazz.getModule().getUnit().getConfig().getJointCompilationOptions() != null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class SpockNames {
public static final String SPOCK_TMP_THROWABLE = "$spock_tmp_throwable";
public static final String SPOCK_VALUE = "$spock_value";
public static final String VALUE_RECORDER = "$spock_valueRecorder";
public static final String WHERE_VARIABLE_VALUES = "$spock_whereVariableValues";
/**
* Name of the method {@link ISpockMockObject#$spock_get()}.
*/
Expand Down
Loading
Loading