Spock Framework Testing Guide: BDD for Java and Groovy

Spock Framework Testing Guide: BDD for Java and Groovy

Spock is a testing and specification framework for Java and Groovy applications. It combines JUnit's test runner compatibility with a highly expressive Groovy-based DSL. The result is tests that read like documentation and fail with clear, informative messages.

Why Spock

Spock's given/when/then structure maps directly to the way developers think about behavior. Built-in mocking, stubbing, and spying remove the need for Mockito. Data-driven tests are first-class with where blocks. And Groovy's dynamic syntax eliminates much of Java's ceremony.

Setup

Add Spock to your Gradle build:

// build.gradle
plugins {
    id 'groovy'
}

dependencies {
    testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0'
    testImplementation 'org.apache.groovy:groovy:4.0.15'
    // For Spring Boot projects
    testImplementation 'org.spockframework:spock-spring:2.3-groovy-4.0'
}

test {
    useJUnitPlatform()
}

Spock test classes extend Specification and live in src/test/groovy.

Basic Specification

import spock.lang.Specification

class CalculatorSpec extends Specification {

    def calculator = new Calculator()

    def "adding two positive numbers returns their sum"() {
        given: "two positive numbers"
        def a = 5
        def b = 3

        when: "they are added"
        def result = calculator.add(a, b)

        then: "the result equals their sum"
        result == 8
    }

    def "dividing by zero throws ArithmeticException"() {
        when:
        calculator.divide(10, 0)

        then:
        thrown(ArithmeticException)
    }
}

Every def "..."() method is a test. The string is the test name — it appears exactly in test reports.

Given/When/Then Blocks

Spock enforces structure through labeled blocks:

  • given (or setup): preconditions, initialization
  • when: the action under test
  • then: assertions and expectations
  • and: continuation of any block
  • expect: shorthand combining when + then for simple assertions
  • cleanup: teardown, always runs
def "user registration sends a welcome email"() {
    given: "a new user registration request"
    def request = new RegistrationRequest(email: "user@example.com", name: "Alice")
    def emailService = Mock(EmailService)
    def userService = new UserService(emailService)

    when: "the user registers"
    def user = userService.register(request)

    then: "a welcome email is sent"
    1 * emailService.sendWelcome(user.email)

    and: "the user is created with correct data"
    user.email == "user@example.com"
    user.name == "Alice"
}

Expect Block for Simple Cases

When there's no clear action/assertion separation, expect is cleaner:

def "Math.max returns the larger value"() {
    expect:
    Math.max(3, 7) == 7
    Math.max(-1, 0) == 0
    Math.max(5, 5) == 5
}

Mocking and Stubbing

Spock has built-in mock support — no Mockito needed:

def "fetching user by ID returns null for unknown users"() {
    given:
    def repo = Mock(UserRepository)
    def service = new UserService(repo)

    repo.findById(99L) >> null  // stubbing: return null

    when:
    def result = service.findUser(99L)

    then:
    result == null
}

Stub returns a value. Mock verifies interactions. Spy wraps a real object:

// Stub — just return values
def repo = Stub(UserRepository)
repo.findAll() >> [new User(id: 1), new User(id: 2)]

// Mock — verify and optionally stub
def emailService = Mock(EmailService)
1 * emailService.send(_)          // must be called exactly once
0 * emailService.sendBulk(_, _)   // must never be called

// Spy — delegate to real implementation
def service = Spy(NotificationService)
service.send("msg") >> "mocked"   // override specific method

Interaction Verification

Spock's interaction syntax is concise and expressive:

then:
// Cardinality
1 * service.method()        // exactly once
2 * service.method()        // exactly twice
(1..3) * service.method()   // between 1 and 3 times
_ * service.method()        // any number of times
0 * service.method()        // never

// Argument constraints
1 * service.save(_ as User)              // any User
1 * service.save({ it.age > 18 })        // closure constraint
1 * service.save(!null)                  // not null
1 * service.save("alice", _)             // first arg "alice", second anything

Exception Testing

def "withdrawing more than balance throws InsufficientFundsException"() {
    given:
    def account = new BankAccount(balance: 100)

    when:
    account.withdraw(200)

    then:
    def e = thrown(InsufficientFundsException)
    e.message == "Insufficient funds"
    e.available == 100
    e.requested == 200
}

def "no exception is thrown for valid withdrawal"() {
    given:
    def account = new BankAccount(balance: 100)

    when:
    account.withdraw(50)

    then:
    notThrown(InsufficientFundsException)
}

Data-Driven Testing with Where

The where block turns a single spec into a parameterized test:

def "password strength validation"() {
    expect:
    validator.isStrong(password) == expected

    where:
    password          || expected
    "abc"             || false
    "abc123"          || false
    "Abc123!"         || true
    "SuperSecure1#"   || true
    ""                || false
}

The || separator is cosmetic but conventional for input/output separation. Spock generates one test per row and names each with the data values.

Shared Fields and Setup

class OrderServiceSpec extends Specification {

    // Shared across all feature methods (use with care — shared state)
    @Shared
    def sharedConfig = loadConfig()

    // Recreated for each feature method
    def orderRepo = Mock(OrderRepository)
    def paymentGateway = Mock(PaymentGateway)
    def service = new OrderService(orderRepo, paymentGateway)

    def setup() {
        // Runs before each feature method
        orderRepo.findById(_) >> Optional.empty()
    }

    def cleanup() {
        // Runs after each feature method
    }

    def setupSpec() {
        // Runs once before all feature methods
    }

    def cleanupSpec() {
        // Runs once after all feature methods
    }
}

Spring Boot Integration

@SpringBootTest
class OrderControllerSpec extends Specification {

    @Autowired
    MockMvc mockMvc

    @MockBean
    OrderService orderService

    def "GET /orders returns list of orders"() {
        given:
        orderService.findAll() >> [new Order(id: 1, total: 50.0)]

        when:
        def response = mockMvc.perform(get("/orders"))
                              .andReturn()
                              .response

        then:
        response.status == 200
        response.contentAsString.contains('"id":1')
    }
}

Running Tests

./gradlew test
./gradlew <span class="hljs-built_in">test --tests <span class="hljs-string">"*OrderServiceSpec*"
./gradlew <span class="hljs-built_in">test --info   <span class="hljs-comment"># verbose output

HTML reports appear in build/reports/tests/test/index.html.

When to Use Spock

Spock excels for Java/Groovy projects where readable tests matter — service layer tests, integration tests, and any scenario with complex setup or data-driven cases. The learning curve is low if your team knows JUnit; the expressiveness payoff is immediate.

Read more