Skip to content
AgileLogics
8 min read

Testing with Spock

Prerequisites: Classes and objects, Exceptions and error handling

Spock is a testing and specification framework for Java and Groovy applications. Its tests read like specifications, its data tables make parameterized tests trivial, and its failure messages show exactly what went wrong. Spock runs on the JUnit Platform, so Gradle, Maven, and IDEs run it like any JUnit test.

The examples in this lesson were run with Spock 2.4 on Groovy 5.1 (Spock publishes one artifact per Groovy major version; at the time of writing the latest was built for Groovy 5). The Groovy runner prints a one-line JUnit summary; the time= value varies from run to run.

A first specification

// illustrative — run with Spock on the classpath
import spock.lang.Specification

class StackSpec extends Specification {
    def "push adds an element to the top"() {
        given:
        def stack = new ArrayDeque<String>()

        when:
        stack.push('a')
        stack.push('b')

        then:
        stack.size() == 2
        stack.peek() == 'b'
    }

    def "pop on an empty stack throws"() {
        given:
        def stack = new ArrayDeque<String>()

        when:
        stack.pop()

        then:
        thrown(NoSuchElementException)
    }
}
JUnit5 launcher: passed=2, aborted=0, failed=0, skipped=0, time=132ms
  • A spec extends Specification; each feature method has a string name.
  • Blocks structure the test: given: (setup), when: (action), then: (expectations), expect: (action and check in one), where: (data), cleanup:.
  • Every line in then: and expect: is an implicit assertion — no assert keyword needed.

Data-driven tests

// illustrative — run with Spock on the classpath
import spock.lang.Specification

class MathSpec extends Specification {
    def "max of #a and #b is #c"() {
        expect:
        Math.max(a, b) == c

        where:
        a | b || c
        1 | 3 || 3
        7 | 4 || 7
        0 | 0 || 0
        2 | 2 || 5
    }
}
JUnit5 launcher: passed=4, aborted=0, failed=1, skipped=0, time=159ms

Failures (1):
  Spock:MathSpec:max of #a and #b is #c:max of 2 and 2 is 5
    MethodSource [className = 'MathSpec', methodName = 'max of #a and #b is #c', methodParameterTypes = 'java.lang.Object, java.lang.Object, java.lang.Object']
    => Condition not satisfied:

Math.max(a, b) == c
|    |   |  |  |  |
|    2   2  2  |  5
|              false
class java.lang.Math

The where: block is a data table: each row runs as its own test, and #a in the name is replaced with the row's values. The deliberately wrong last row shows Spock's power assertion output: the value of every sub-expression. (The summary counts the parent feature as well as its four rows, and the output continues with a stack trace, trimmed here.)

Mocks and stubs

// illustrative — run with Spock on the classpath
import spock.lang.Specification

class CheckoutSpec extends Specification {
    def "charges the card with the cart total"() {
        given:
        def payments = Mock(PaymentGateway)
        def checkout = new Checkout(payments: payments)

        when:
        def ok = checkout.pay([apple: 0.5, bread: 2.25], 'card-1')

        then:
        1 * payments.charge('card-1', 2.75) >> true
        0 * payments.refund(_)
        ok
    }

    def "reports a declined card"() {
        given:
        def payments = Stub(PaymentGateway) { charge(_, _) >> false }

        expect:
        !new Checkout(payments: payments).pay([tea: 3], 'card-2')
    }
}

interface PaymentGateway {
    boolean charge(String card, BigDecimal amount)
    void refund(String card)
}

class Checkout {
    PaymentGateway payments
    boolean pay(Map<String, BigDecimal> items, String card) {
        payments.charge(card, items.values().sum() as BigDecimal)
    }
}
JUnit5 launcher: passed=2, aborted=0, failed=0, skipped=0, time=290ms
  • Mock(Type) creates a mock; 1 * payments.charge('card-1', 2.75) says "expect exactly one call with these arguments", and >> true gives its return value.
  • _ matches any argument; 0 * ... asserts a call never happens.
  • Stub(Type) only returns values and never fails on interactions.

Useful extras

  • @Unroll is the default in Spock 2, so every data row is reported separately.
  • setup(), cleanup(), setupSpec(), and cleanupSpec() run around features; @Shared fields are shared between them.
  • @Ignore, @IgnoreIf({ os.windows }), @Timeout(5), and @Retry control execution.
  • with(object) { name == 'Ada'; age == 36 } checks several properties at once, and verifyAll { } reports every failed condition instead of stopping at the first.

Setting up Spock in Gradle

// illustrative — build.gradle
dependencies {
    testImplementation platform('org.spockframework:spock-bom:2.4-groovy-5.0')
    testImplementation 'org.spockframework:spock-core'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') { useJUnitPlatform() }

Specs go in src/test/groovy and are named *Spec.groovy. Spock works just as well for testing Java code — many Java teams use it for that reason alone.

Without Spock: JUnit and plain assert

Groovy also runs JUnit 5/6 test classes directly (groovy MyTest.groovy uses the bundled launcher), and for small scripts, power-asserting with assert is often all you need.