Spock Data-Driven Testing: @Unroll, @Where, and Parameterized Specs
Spock's data-driven testing features turn a single spec into a parameterized test suite with almost no boilerplate. The where block, data tables, and @Unroll annotation work together to produce clear, readable test output for every data combination.
The Where Block
Add a where block to any feature method and Spock runs it once per row:
def "maximum of two numbers"() {
expect:
Math.max(a, b) == expected
where:
a | b | expected
1 | 3 | 3
7 | 4 | 7
0 | 0 | 0
-1 | 1 | 1
}Spock generates four independent test runs. If one fails, the others still execute.
Unrolling Test Names
By default, Spock names parameterized tests maximum of two numbers[0], [1], etc. With @Unroll, each iteration gets its own descriptive name using variable interpolation:
@Unroll
def "max(#a, #b) == #expected"() {
expect:
Math.max(a, b) == expected
where:
a | b | expected
1 | 3 | 3
7 | 4 | 7
0 | 0 | 0
}Output in test reports:
✓ max(1, 3) == 3
✓ max(7, 4) == 7
✓ max(0, 0) == 0In Spock 2.0+, @Unroll is the default behavior — you don't need the annotation unless overriding class-level settings.
The Double Pipe Separator
The || separator is cosmetic — it visually separates inputs from outputs:
where:
input || expected
"hello" || "HELLO"
"world" || "WORLD"
"" || ""
"MixedCase" || "MIXEDCASE"Spock treats | and || identically; the convention is to use || before expected values.
Data Pipes
For programmatic data, use the << (left shift) operator:
def "all positive numbers are valid"() {
expect:
validator.isPositive(n)
where:
n << [1, 5, 100, Integer.MAX_VALUE]
}Combine multiple pipes:
def "cartesian product of two lists"() {
expect:
a + b > 0
where:
a << [1, 2, 3]
b << [10, 20, 30]
}Multiple pipes iterate in parallel (not Cartesian), so both lists must have the same length.
Derived Variables
Variables in where can reference earlier ones in the same block:
where:
base | exponent | expected
2 | 3 | base ** exponent // 8
3 | 2 | base ** exponent // 9
10 | 0 | base ** exponent // 1Derived variables make the expected column self-documenting.
Data Tables vs Data Pipes
Use a data table when you have fixed, readable input/output pairs. Use pipes when data comes from code — lists, generators, or external sources:
// Table: clear, readable
where:
email | valid
"user@example.com" | true
"invalid-email" | false
"" | false
"user@" | false
// Pipe: programmatic
where:
input << EmailFixtures.validEmails()Loading Data from External Sources
@Unroll
def "validate user #name"() {
expect:
service.validate(user)
where:
[name, user] << loadUsersFromCsv("src/test/resources/users.csv")
}
private List loadUsersFromCsv(String path) {
new File(path).readLines().drop(1).collect { line ->
def parts = line.split(",")
[parts[0], new User(name: parts[0], email: parts[1])]
}
}Or from a database fixture:
where:
user << UserFixtures.allValidUsers()Combining Where with Given/When/Then
The where block works with all block styles:
@Unroll
def "creating order with #quantity items calculates correct total"() {
given:
def order = new Order()
when:
quantity.times { order.addItem(new Item(price: itemPrice)) }
then:
order.total == expectedTotal
where:
quantity | itemPrice | expectedTotal
1 | 10.00 | 10.00
3 | 5.00 | 15.00
0 | 9.99 | 0.00
}Naming Strategies for Unrolled Tests
@Unroll supports full Groovy expression interpolation in test names:
@Unroll
def "#username (role: #user.role) can#access /admin"() {
// ...
where:
username | user | access
"alice" | new User(role: "ADMIN") | ""
"bob" | new User(role: "USER") | "not"
}Output:
✓ alice (role: ADMIN) can /admin
✗ bob (role: USER) cannot /admin@Unroll at Class Level
Apply to all feature methods in a spec:
@Unroll
class ValidationSpec extends Specification {
def "email #email is #description"() {
expect:
EmailValidator.isValid(email) == valid
where:
email | valid | description
"a@b.com" | true | "valid"
"notanemail" | false | "invalid"
}
def "phone #number is #status"() {
expect:
PhoneValidator.isValid(number) == valid
where:
number | valid | status
"+1234567890"| true | "valid"
"abc" | false | "invalid"
}
}Ignoring Rows
Use @IgnoreIf or filter within the where block:
@Unroll
@IgnoreIf({ System.getenv("CI") }) // skip slow tests in CI
def "complex scenario #input"() {
// ...
}Or filter the data source:
where:
data << allTestData.findAll { it.enabled }Combining @Unroll with @Retry
For flaky data-driven tests:
@Unroll
@Retry(count = 2)
def "HTTP request to #url succeeds"() {
when:
def response = client.get(url)
then:
response.status == 200
where:
url << externalEndpoints
}When Data-Driven Testing Fits
Use data-driven specs when:
- Multiple input combinations map to the same behavior
- You're testing boundary values (0, negative, max, empty string)
- You have a truth table (validation rules, permission checks)
- You're testing pure functions
Avoid it when each row requires significantly different setup — separate specs are clearer in those cases.
Data-driven testing is where Spock's design pays off most visibly: the where block, @Unroll, and variable interpolation together produce parameterized tests that are easier to read, maintain, and extend than any JUnit equivalent.