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 methodInteraction 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 anythingException 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 outputHTML 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.