Groovy testing with Spock Framework. 12+ examples covering given/when/then, data-driven testing, mocking, stubbing, exception testing, @Unroll. Groovy 5.0.6.
“Code without tests is bad code.”
Michael Feathers, Working Effectively with Legacy Code
Last Updated: July 2026 | Tested on: Groovy 5.0.6, Spock 2.4, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 25 minutes
Testing is one of those things every developer knows they should do, but too often the testing framework itself gets in the way. Verbose setup, cryptic assertion failures, boilerplate everywhere. That is where the Spock Framework changes the game. Built specifically for Groovy (and Java) developers, Spock gives you a testing DSL that reads like a specification document. Your tests describe behavior in plain language with given, when, then blocks.
This Groovy Spock testing guide walks you through what you need to get started: writing your first specification, using data-driven testing with where blocks, mocking and stubbing dependencies, testing exceptions, and building real-world service tests. All with working examples and actual output.
If you want to sharpen your assertion skills first, check out our Groovy Assert and Power Assert guide. And once you are comfortable with Spock basics, the full Groovy Testing Guide covers advanced patterns and integration testing strategies.
Table of Contents
What Is the Spock Framework?
Spock is a testing and specification framework for Groovy and Java applications. It combines the best ideas from JUnit, Mockito, and JBehave into a single, cohesive framework. Spock tests are called specifications, and they extend the spock.lang.Specification base class.
According to the official Spock documentation, Spock is inspired by JUnit, RSpec, jMock, Mockito, Groovy, Scala, and Vulcans. It runs on the JUnit Platform and is compatible with most IDEs, build tools, and CI servers.
Key Points:
- Specifications extend
spock.lang.Specification– that is the only requirement - Tests use expressive blocks:
given,when,then,expect,where,setup,cleanup - Built-in mocking and stubbing – no separate library needed
- Data-driven testing with
whereblocks and@Unrollfor parameterized tests - Power assertions that show the value of each sub-expression on failure
- Runs on JUnit Platform – works with Gradle, Maven, and all major IDEs
Why Use Spock for Groovy Testing?
You might be wondering – why not just use JUnit 5? It works fine with Groovy. Here is why the Spock Framework is worth learning:
- Readable specifications –
given/when/thenblocks make tests self-documenting - Powerful assertions – every boolean expression in a
thenblock is automatically an assertion, with detailed failure messages - Built-in mocking – no need for Mockito, EasyMock, or any external mocking library
- Data-driven testing – the
whereblock makes parameterized tests elegant and easy to read - Groovy-native – built in Groovy, for Groovy, leveraging closures, GStrings, and dynamic features
- Great error reporting – when a test fails, Spock shows you exactly what went wrong, not just “expected true but got false”
- Can test Java code too – write Spock specs in Groovy to test your Java classes
That said, if your team is 100% Java and unfamiliar with Groovy, JUnit 5 is a perfectly valid choice. But for Groovy projects, or any team willing to write tests in Groovy, Spock is hard to beat.
Setting Up Spock
Before writing your first spec, you need to add Spock to your project. Here is the Gradle setup (the most common choice for Groovy projects):
build.gradle – Spock Setup
plugins {
id 'groovy'
}
repositories {
mavenCentral()
}
dependencies {
// Groovy
implementation 'org.apache.groovy:groovy:5.0.0-alpha-13'
// Spock Framework
testImplementation 'org.spockframework:spock-core:2.4-M4-groovy-4.0'
// JUnit Platform (required for Spock 2.x)
testImplementation platform('org.junit:junit-bom:5.11.4')
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
For Maven users, add the equivalent dependency to your pom.xml under <dependencies>. The key artifact is spock-core from the org.spockframework group.
Place your Spock specifications in src/test/groovy/ with the file extension .groovy. Each spec class should end with Spec (e.g., CalculatorSpec.groovy).
12+ Practical Examples
Before the examples, here is how Spock feature methods are structured using blocks like given, when, then, expect, and where:
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
Example 1: Your First Spock Specification (given/when/then)
What we’re doing: Writing a basic Spock specification with the given/when/then block structure to test a simple calculator.
Example 1: Basic given/when/then
import spock.lang.Specification
class Calculator {
int add(int a, int b) { a + b }
int subtract(int a, int b) { a - b }
int multiply(int a, int b) { a * b }
}
class CalculatorSpec extends Specification {
def "should add two numbers correctly"() {
given: "a calculator instance"
def calc = new Calculator()
when: "we add 3 and 7"
def result = calc.add(3, 7)
then: "the result is 10"
result == 10
}
def "should subtract two numbers correctly"() {
given: "a calculator"
def calc = new Calculator()
when: "we subtract 4 from 10"
def result = calc.subtract(10, 4)
then: "the result is 6"
result == 6
}
def "should multiply two numbers correctly"() {
given: "a calculator"
def calc = new Calculator()
when: "we multiply 5 and 3"
def result = calc.multiply(5, 3)
then: "the result is 15"
result == 15
}
}
Output
CalculatorSpec - should add two numbers correctly PASSED - should subtract two numbers correctly PASSED - should multiply two numbers correctly PASSED 3 tests completed, 0 failed
What happened here: Each test method is a feature method with a descriptive string name. The given block sets up preconditions, when performs the action under test, and then contains assertions. Every expression in the then block is automatically treated as an assertion – no need for assert or assertEquals().
Example 2: Using expect Blocks for Simple Tests
What we’re doing: Using the expect block for tests where stimulus and expected response can be expressed in a single expression.
Example 2: expect Block
import spock.lang.Specification
class MathUtilsSpec extends Specification {
def "Math.max returns the larger of two numbers"() {
expect:
Math.max(3, 7) == 7
Math.max(-1, -5) == -1
Math.max(0, 0) == 0
}
def "String length works as expected"() {
expect:
"hello".length() == 5
"".length() == 0
"Spock".toUpperCase() == "SPOCK"
}
def "List operations produce correct results"() {
expect:
[1, 2, 3].sum() == 6
[3, 1, 2].sort() == [1, 2, 3]
[1, 2, 3].reverse() == [3, 2, 1]
}
}
Output
MathUtilsSpec - Math.max returns the larger of two numbers PASSED - String length works as expected PASSED - List operations produce correct results PASSED 3 tests completed, 0 failed
What happened here: The expect block is a shorthand for cases where you do not need separate when and then blocks. Every line in expect is an assertion. Use it for pure functions and simple checks where there is no side effect to observe.
Example 3: Data-Driven Testing with where Blocks
What we’re doing: Using the where block to run the same test logic with multiple sets of input data.
Example 3: where Block (Data-Driven Testing)
import spock.lang.Specification
class Calculator {
int add(int a, int b) { a + b }
}
class DataDrivenSpec extends Specification {
def "adding #a and #b should return #expected"() {
given:
def calc = new Calculator()
expect:
calc.add(a, b) == expected
where:
a | b || expected
1 | 2 || 3
0 | 0 || 0
-1 | 1 || 0
10 | 20 || 30
-5 | -3 || -8
99 | 1 || 100
}
}
Output
DataDrivenSpec - adding 1 and 2 should return 3 PASSED - adding 0 and 0 should return 0 PASSED - adding -1 and 1 should return 0 PASSED - adding 10 and 20 should return 30 PASSED - adding -5 and -3 should return -8 PASSED - adding 99 and 1 should return 100 PASSED 6 tests completed, 0 failed
What happened here: The where block defines a data table. The | separates input columns, and || separates inputs from the expected output. Spock runs the test once for each row. The #a, #b, and #expected placeholders in the method name get replaced with actual values in the test report.
Example 4: @Unroll for Parameterized Test Names
What we’re doing: Using the @Unroll annotation to generate individual test names for each row in a data-driven test.
Example 4: @Unroll Annotation
import spock.lang.Specification
import spock.lang.Unroll
class StringValidatorSpec extends Specification {
boolean isValidEmail(String email) {
email != null && email.contains('@') && email.contains('.')
}
@Unroll
def "isValidEmail('#email') should return #expected"() {
expect:
isValidEmail(email) == expected
where:
email || expected
"user@example.com" || true
"admin@server.org" || true
"no-at-sign.com" || false
"missing@dot" || false
null || false
"" || false
"valid@test.io" || true
}
}
Output
StringValidatorSpec
- isValidEmail('user@example.com') should return true PASSED
- isValidEmail('admin@server.org') should return true PASSED
- isValidEmail('no-at-sign.com') should return false PASSED
- isValidEmail('missing@dot') should return false PASSED
- isValidEmail('null') should return false PASSED
- isValidEmail('') should return false PASSED
- isValidEmail('valid@test.io') should return true PASSED
7 tests completed, 0 failed
What happened here: The @Unroll annotation tells Spock to report each data row as a separate test in the output. Without @Unroll, all rows would show as a single test. The #email and #expected placeholders in the method name are replaced with actual values from each row, making the test report crystal clear.
Example 5: setup/cleanup and setupSpec/cleanupSpec Lifecycle
What we’re doing: Demonstrating the Spock lifecycle methods for per-test and per-spec setup and teardown.
Example 5: Lifecycle Methods
import spock.lang.Specification
class LifecycleSpec extends Specification {
// Shared field - initialized once for the entire spec
@spock.lang.Shared
def log = []
// Runs once before ALL feature methods
def setupSpec() {
log << "setupSpec"
println "setupSpec: runs once before all tests"
}
// Runs before EACH feature method
def setup() {
log << "setup"
println " setup: runs before each test"
}
// Runs after EACH feature method
def cleanup() {
log << "cleanup"
println " cleanup: runs after each test"
}
// Runs once after ALL feature methods
def cleanupSpec() {
log << "cleanupSpec"
println "cleanupSpec: runs once after all tests"
println "Full lifecycle order: ${log}"
}
def "first test"() {
expect:
log.contains("setupSpec")
println " Running first test"
true
}
def "second test"() {
expect:
log.count("setup") >= 2
println " Running second test"
true
}
}
Output
setupSpec: runs once before all tests
setup: runs before each test
Running first test
cleanup: runs after each test
setup: runs before each test
Running second test
cleanup: runs after each test
cleanupSpec: runs once after all tests
Full lifecycle order: [setupSpec, setup, cleanup, setup, cleanup, cleanupSpec]
2 tests completed, 0 failed
What happened here: Spock has four lifecycle hooks. setupSpec() runs once before the entire spec, setup() runs before each test, cleanup() runs after each test, and cleanupSpec() runs once after all tests. Fields used in setupSpec/cleanupSpec must be annotated with @Shared because those methods run in a shared context.
The following diagram shows the full Spock specification lifecycle, from setupSpec() through each feature method’s setup()/cleanup() cycle, to cleanupSpec():
Example 6: Mocking with Mock()
What we’re doing: Creating mock objects using Spock’s built-in Mock() method to isolate the class under test from its dependencies.
Example 6: Mocking with Mock()
import spock.lang.Specification
// The dependency interface
interface UserRepository {
Map findById(int id)
List findAll()
boolean save(Map user)
}
// The class under test
class UserService {
UserRepository repository
UserService(UserRepository repo) {
this.repository = repo
}
String getUserName(int id) {
def user = repository.findById(id)
return user?.name ?: "Unknown"
}
int getUserCount() {
return repository.findAll().size()
}
}
class MockSpec extends Specification {
def "should return user name from repository"() {
given: "a mock repository and a service"
def mockRepo = Mock(UserRepository)
def service = new UserService(mockRepo)
and: "the mock returns a user for id 1"
mockRepo.findById(1) >> [id: 1, name: "Alice"]
when: "we fetch the user name"
def name = service.getUserName(1)
then: "the name matches"
name == "Alice"
}
def "should return Unknown for missing user"() {
given:
def mockRepo = Mock(UserRepository)
def service = new UserService(mockRepo)
and: "the mock returns null for id 999"
mockRepo.findById(999) >> null
when:
def name = service.getUserName(999)
then:
name == "Unknown"
}
def "should count all users"() {
given:
def mockRepo = Mock(UserRepository)
def service = new UserService(mockRepo)
and:
mockRepo.findAll() >> [
[id: 1, name: "Alice"],
[id: 2, name: "Bob"],
[id: 3, name: "Charlie"]
]
when:
def count = service.getUserCount()
then:
count == 3
}
}
Output
MockSpec - should return user name from repository PASSED - should return Unknown for missing user PASSED - should count all users PASSED 3 tests completed, 0 failed
What happened here: Mock(UserRepository) creates a mock instance of the interface. The >> operator (called the “right-shift” or “respond with” operator) defines what the mock returns when a method is called. By default, mock methods return null, 0, or false depending on the return type. No Mockito, no PowerMock – it is all built in.
Example 7: Stubbing with Stub()
What we’re doing: Using Stub() instead of Mock() when we only need to define return values without verifying interactions.
Example 7: Stubbing
import spock.lang.Specification
interface PricingService {
BigDecimal getPrice(String productId)
BigDecimal getDiscount(String customerId)
}
class OrderCalculator {
PricingService pricing
OrderCalculator(PricingService pricing) {
this.pricing = pricing
}
BigDecimal calculateTotal(String productId, int quantity, String customerId) {
def price = pricing.getPrice(productId)
def discount = pricing.getDiscount(customerId)
def subtotal = price * quantity
return subtotal - (subtotal * discount)
}
}
class StubSpec extends Specification {
def "should calculate total with discount"() {
given: "a stubbed pricing service"
def pricingStub = Stub(PricingService)
pricingStub.getPrice("PROD-001") >> 25.00
pricingStub.getDiscount("CUST-VIP") >> 0.10
and: "an order calculator using the stub"
def calculator = new OrderCalculator(pricingStub)
when:
def total = calculator.calculateTotal("PROD-001", 4, "CUST-VIP")
then:
total == 90.00 // (25 * 4) - (100 * 0.10) = 90
}
def "should handle zero discount"() {
given:
def pricingStub = Stub(PricingService)
pricingStub.getPrice(_) >> 50.00 // any argument
pricingStub.getDiscount(_) >> 0.00 // no discount
def calculator = new OrderCalculator(pricingStub)
when:
def total = calculator.calculateTotal("ANY", 3, "ANY")
then:
total == 150.00
}
}
Output
StubSpec - should calculate total with discount PASSED - should handle zero discount PASSED 2 tests completed, 0 failed
What happened here: Stub() creates a test double that only provides canned responses. Unlike Mock(), you cannot verify interactions on a stub – it is purely for providing data. The _ (underscore) is Spock’s wildcard argument matcher, meaning “match any argument.” Use stubs when you care about the return values, and mocks when you need to verify that specific methods were called.
Example 8: Interaction-Based Testing (Verifying Method Calls)
What we’re doing: Verifying that specific methods are called the expected number of times using Spock’s interaction-based testing.
Example 8: Interaction-Based Testing
import spock.lang.Specification
interface NotificationService {
void sendEmail(String to, String message)
void sendSms(String to, String message)
void log(String message)
}
class OrderProcessor {
NotificationService notifications
OrderProcessor(NotificationService notifications) {
this.notifications = notifications
}
void processOrder(String customerEmail, String phone, String orderId) {
// Business logic
notifications.log("Processing order: ${orderId}")
notifications.sendEmail(customerEmail, "Order ${orderId} confirmed!")
notifications.sendSms(phone, "Order ${orderId} is on its way!")
notifications.log("Order ${orderId} notifications sent")
}
}
class InteractionSpec extends Specification {
def "processOrder should send email and SMS, and log twice"() {
given:
def mockNotify = Mock(NotificationService)
def processor = new OrderProcessor(mockNotify)
when:
processor.processOrder("alice@test.com", "+1234567890", "ORD-001")
then: "email is sent exactly once"
1 * mockNotify.sendEmail("alice@test.com", "Order ORD-001 confirmed!")
and: "SMS is sent exactly once"
1 * mockNotify.sendSms("+1234567890", "Order ORD-001 is on its way!")
and: "log is called exactly twice"
2 * mockNotify.log(_)
}
def "processOrder should not send email to wrong address"() {
given:
def mockNotify = Mock(NotificationService)
def processor = new OrderProcessor(mockNotify)
when:
processor.processOrder("alice@test.com", "+1234567890", "ORD-002")
then: "email to bob never happens"
0 * mockNotify.sendEmail("bob@test.com", _)
and: "email to alice happens once"
1 * mockNotify.sendEmail("alice@test.com", _)
and: "allow all other interactions"
_ * mockNotify._(*_)
}
}
Output
InteractionSpec - processOrder should send email and SMS, and log twice PASSED - processOrder should not send email to wrong address PASSED 2 tests completed, 0 failed
What happened here: Interaction verification is one of Spock’s superpowers. The syntax 1 * mockNotify.sendEmail(...) asserts that sendEmail was called exactly once with those arguments. Use 0 * to verify something was never called. The wildcard _ * mockNotify._(*_) means “allow any number of calls to any method with any arguments” – useful to avoid strict mocking failures for calls you do not care about.
Example 9: Exception Testing with thrown()
What we’re doing: Testing that methods throw the expected exceptions using Spock’s thrown() method.
Example 9: Exception Testing
import spock.lang.Specification
class AccountService {
BigDecimal balance = 0
void deposit(BigDecimal amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive, got: ${amount}")
}
balance += amount
}
void withdraw(BigDecimal amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive")
}
if (amount > balance) {
throw new InsufficientFundsException("Cannot withdraw ${amount}, balance is ${balance}")
}
balance -= amount
}
}
class InsufficientFundsException extends RuntimeException {
InsufficientFundsException(String message) { super(message) }
}
class ExceptionSpec extends Specification {
def "deposit should reject negative amounts"() {
given:
def account = new AccountService()
when:
account.deposit(-50)
then:
def ex = thrown(IllegalArgumentException)
ex.message == "Deposit amount must be positive, got: -50"
}
def "deposit should reject zero amount"() {
given:
def account = new AccountService()
when:
account.deposit(0)
then:
thrown(IllegalArgumentException)
}
def "withdraw should throw when insufficient funds"() {
given:
def account = new AccountService()
account.deposit(100)
when:
account.withdraw(200)
then:
def ex = thrown(InsufficientFundsException)
ex.message.contains("Cannot withdraw 200")
ex.message.contains("balance is 100")
}
def "valid deposit should not throw"() {
given:
def account = new AccountService()
when:
account.deposit(100)
then:
notThrown(IllegalArgumentException)
account.balance == 100
}
}
Output
ExceptionSpec - deposit should reject negative amounts PASSED - deposit should reject zero amount PASSED - withdraw should throw when insufficient funds PASSED - valid deposit should not throw PASSED 4 tests completed, 0 failed
What happened here: thrown(ExceptionType) is placed in the then block and verifies that the when block threw the specified exception. You can capture the exception in a variable to inspect its message or other properties. The notThrown() method does the opposite – it asserts that no exception of the given type was thrown.
Example 10: Data Pipes and Multiple Data Sources in where
What we’re doing: Using data pipes and alternative data sources in where blocks beyond the standard table format.
Example 10: Data Pipes
import spock.lang.Specification
import spock.lang.Unroll
class DataPipeSpec extends Specification {
@Unroll
def "square of #x is #expected"() {
expect:
x * x == expected
where:
// Data pipe syntax - left shift from a list
x << [1, 2, 3, 4, 5, 10]
expected << [1, 4, 9, 16, 25, 100]
}
@Unroll
def "'#word' has length #len"() {
expect:
word.length() == len
where:
// Derived data variable
word << ["Spock", "Groovy", "Test", "JVM"]
len = word.length() // computed from the data pipe
}
@Unroll
def "#input is #description"() {
expect:
(input > 0) == positive
where:
input | positive
5 | true
-3 | false
0 | false
100 | true
// Derived variable for descriptive naming
description = positive ? "positive" : "not positive"
}
}
Output
DataPipeSpec - square of 1 is 1 PASSED - square of 2 is 4 PASSED - square of 3 is 9 PASSED - square of 4 is 16 PASSED - square of 5 is 25 PASSED - square of 10 is 100 PASSED - 'Spock' has length 5 PASSED - 'Groovy' has length 6 PASSED - 'Test' has length 4 PASSED - 'JVM' has length 3 PASSED - 5 is positive PASSED - -3 is not positive PASSED - 0 is not positive PASSED - 100 is positive PASSED 14 tests completed, 0 failed
What happened here: Data pipes use the << operator to feed values from a list (or any iterable) into test variables. You can also define derived data variables using = in the where block – these are computed from other data variables and are great for generating descriptive test names or reusable computed values.
Example 11: Spying with Spy()
What we’re doing: Using Spy() to wrap a real object and selectively override or verify its methods.
Example 11: Spy()
import spock.lang.Specification
class EmailFormatter {
String format(String name, String subject) {
def greeting = buildGreeting(name)
def body = buildBody(subject)
return "${greeting}\n${body}"
}
String buildGreeting(String name) {
return "Dear ${name},"
}
String buildBody(String subject) {
return "This is regarding: ${subject}."
}
}
class SpySpec extends Specification {
def "spy calls real methods by default"() {
given:
def formatter = Spy(EmailFormatter)
when:
def result = formatter.format("Alice", "Meeting")
then:
result == "Dear Alice,\nThis is regarding: Meeting."
and: "verify real methods were called"
1 * formatter.buildGreeting("Alice")
1 * formatter.buildBody("Meeting")
}
def "spy can override specific methods"() {
given:
def formatter = Spy(EmailFormatter)
and: "override just the greeting"
formatter.buildGreeting(_) >> "Hello there!"
when:
def result = formatter.format("Alice", "Meeting")
then: "greeting is overridden, body is real"
result == "Hello there!\nThis is regarding: Meeting."
}
}
Output
SpySpec - spy calls real methods by default PASSED - spy can override specific methods PASSED 2 tests completed, 0 failed
What happened here: A Spy() wraps a real object. By default, all method calls go to the real implementation. You can selectively override methods with >> and verify interactions just like with mocks. Spies are useful when you want to test a real class but need to override one expensive or external method. Use them sparingly – if you need many overrides, consider refactoring to use interfaces and mocks instead.
Example 12: Real-World Service Testing Example
What we’re doing: Building a realistic service test that combines mocking, stubbing, exception handling, and interaction verification in a real-world scenario.
Example 12: Real-World Service Test
import spock.lang.Specification
// Domain classes
class Product {
String id
String name
BigDecimal price
int stock
}
// Repository interface
interface ProductRepository {
Product findById(String id)
void save(Product product)
}
// Audit logger interface
interface AuditLogger {
void log(String action, String details)
}
// Service under test
class InventoryService {
ProductRepository repo
AuditLogger audit
InventoryService(ProductRepository repo, AuditLogger audit) {
this.repo = repo
this.audit = audit
}
Product purchaseProduct(String productId, int quantity) {
def product = repo.findById(productId)
if (!product) {
throw new IllegalArgumentException("Product not found: ${productId}")
}
if (product.stock < quantity) {
audit.log("PURCHASE_FAILED", "Insufficient stock for ${product.name}")
throw new IllegalStateException(
"Insufficient stock: requested ${quantity}, available ${product.stock}"
)
}
product.stock -= quantity
repo.save(product)
audit.log("PURCHASE_SUCCESS", "${quantity}x ${product.name} purchased")
return product
}
}
class InventoryServiceSpec extends Specification {
ProductRepository mockRepo = Mock()
AuditLogger mockAudit = Mock()
InventoryService service = new InventoryService(mockRepo, mockAudit)
def "successful purchase reduces stock and logs"() {
given: "a product with 10 in stock"
def laptop = new Product(id: "P1", name: "Laptop", price: 999.99, stock: 10)
mockRepo.findById("P1") >> laptop
when: "purchasing 3 units"
def result = service.purchaseProduct("P1", 3)
then: "stock is reduced"
result.stock == 7
and: "product is saved"
1 * mockRepo.save({ it.stock == 7 })
and: "success is logged"
1 * mockAudit.log("PURCHASE_SUCCESS", "3x Laptop purchased")
}
def "purchase with insufficient stock throws and logs failure"() {
given:
def phone = new Product(id: "P2", name: "Phone", price: 699.99, stock: 2)
mockRepo.findById("P2") >> phone
when:
service.purchaseProduct("P2", 5)
then:
def ex = thrown(IllegalStateException)
ex.message.contains("Insufficient stock")
ex.message.contains("requested 5")
ex.message.contains("available 2")
and: "failure is logged"
1 * mockAudit.log("PURCHASE_FAILED", "Insufficient stock for Phone")
and: "product is NOT saved"
0 * mockRepo.save(_)
}
def "purchase of non-existent product throws"() {
given:
mockRepo.findById("UNKNOWN") >> null
when:
service.purchaseProduct("UNKNOWN", 1)
then:
def ex = thrown(IllegalArgumentException)
ex.message == "Product not found: UNKNOWN"
and: "nothing is logged or saved"
0 * mockAudit.log(_, _)
0 * mockRepo.save(_)
}
}
Output
InventoryServiceSpec - successful purchase reduces stock and logs PASSED - purchase with insufficient stock throws and logs failure PASSED - purchase of non-existent product throws PASSED 3 tests completed, 0 failed
What happened here: This is a realistic service test covering the happy path, an error path, and an edge case. We used Mock() for both the repository and the audit logger. Notice the argument constraint { it.stock == 7 } in the save verification – Spock lets you use closures to assert the arguments passed to mock methods. This approach verifies not just that save was called, but that it was called with the correctly updated product.
Spock vs JUnit Comparison
To put things in perspective, here is the same test written in both JUnit 5 and Spock. This is a good comparison to understand how the Spock Framework simplifies your test code.
JUnit 5 Style (Groovy)
// JUnit 5 approach - works but verbose
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import static org.junit.jupiter.api.Assertions.*
class CalculatorJUnitTest {
@Test
void testAddPositiveNumbers() {
def calc = new Calculator()
assertEquals(10, calc.add(3, 7))
}
@Test
void testAddNegativeNumbers() {
def calc = new Calculator()
assertEquals(-8, calc.add(-5, -3))
}
@ParameterizedTest
@CsvSource(["1,2,3", "0,0,0", "-1,1,0", "10,20,30"])
void testAddParameterized(int a, int b, int expected) {
def calc = new Calculator()
assertEquals(expected, calc.add(a, b))
}
@Test
void testDepositRejectsNegative() {
def account = new AccountService()
assertThrows(IllegalArgumentException) {
account.deposit(-50)
}
}
}
Spock Equivalent
// Spock approach - cleaner, more readable
import spock.lang.Specification
import spock.lang.Unroll
class CalculatorSpockSpec extends Specification {
def "should add two numbers correctly"() {
expect:
new Calculator().add(3, 7) == 10
}
@Unroll
def "adding #a and #b should return #expected"() {
expect:
new Calculator().add(a, b) == expected
where:
a | b || expected
1 | 2 || 3
0 | 0 || 0
-1 | 1 || 0
10 | 20 || 30
}
def "deposit should reject negative amounts"() {
when:
new AccountService().deposit(-50)
then:
thrown(IllegalArgumentException)
}
}
The differences are clear:
| Feature | JUnit 5 | Spock |
|---|---|---|
| Test names | Method names (camelCase) | Descriptive strings with spaces |
| Assertions | assertEquals(), assertTrue() | Plain boolean expressions |
| Parameterized tests | @CsvSource, @MethodSource | where block data tables |
| Mocking | Requires Mockito | Built-in Mock(), Stub(), Spy() |
| Exception testing | assertThrows() | thrown() in then block |
| Failure messages | Basic | Power assertions with sub-expression values |
| Structure | Flat | given/when/then blocks |
Both are excellent testing frameworks. JUnit 5 has a larger ecosystem and wider adoption. Spock is more expressive and productive, especially for Groovy developers. Many teams use Spock for Groovy/Grails projects and JUnit for pure Java projects.
Best Practices and Tips
DO:
- Use descriptive string names for feature methods – they appear in test reports and make failures self-explanatory
- Use
given/when/thenfor state-based tests andexpectfor simple functional tests - Add label descriptions to blocks (e.g.,
given: "a valid user") for documentation - Prefer
Stub()overMock()when you do not need interaction verification - Use
@Unrollwith parameterized method names to get clear test reports - Keep each feature method focused on a single behavior
- Use
@Sharedfor expensive resources that can be safely shared across tests
DON’T:
- Put logic in
thenblocks – only assertions belong there - Over-use
Spy()– it often indicates the code needs refactoring - Verify every single mock interaction – only verify what matters for the behavior under test
- Share mutable state between feature methods without
@Shared - Use
def setup()for heavy initialization – usedef setupSpec()with@Sharedinstead - Mix stubbing (
>>) and verification (N *) in the same interaction – do them separately
Common Pitfalls
Pitfall 1: Assertions Outside then/expect Blocks
Problem: Placing boolean expressions in when or given blocks and expecting them to be assertions.
Pitfall 1: Wrong Block for Assertions
// WRONG - this boolean is ignored, not an assertion!
def "bad test"() {
when:
def result = 2 + 2
result == 5 // This is NOT an assertion here!
then:
true // Test passes even though 4 != 5
}
// CORRECT
def "good test"() {
when:
def result = 2 + 2
then:
result == 4 // This IS an assertion in the then block
}
Solution: Only place assertion expressions in then or expect blocks. Expressions in when and given blocks are executed as regular statements.
Pitfall 2: Stubbing and Verification on the Same Mock Call
Problem: Trying to stub a return value and verify the call count in the same statement.
Pitfall 2: Combined Stubbing and Verification
// If you need both stubbing AND verification, combine them:
def "stubbing and verification together"() {
given:
def repo = Mock(UserRepository)
def service = new UserService(repo)
when:
def name = service.getUserName(1)
then: "stub AND verify in the then block"
1 * repo.findById(1) >> [id: 1, name: "Alice"]
name == "Alice"
}
// Note: the "N * mock.method() >> value" syntax does both
// verification and stubbing in one line
Solution: When you need both stubbing and verification, use the combined syntax N * mock.method(args) >> returnValue in the then block. This verifies the call count and provides the return value at the same time.
Pitfall 3: Forgetting @Shared for setupSpec Fields
Problem: Using instance fields in setupSpec() or cleanupSpec() without @Shared.
Pitfall 3: @Shared Requirement
// WRONG - will fail or behave unexpectedly
class BadSpec extends Specification {
def connection // instance field
def setupSpec() {
connection = createDbConnection() // ERROR!
}
}
// CORRECT - use @Shared
class GoodSpec extends Specification {
@Shared
def connection
def setupSpec() {
connection = createDbConnection() // Works!
}
}
Solution: Spock creates a new instance of the specification class for each feature method. Fields used in setupSpec()/cleanupSpec() must be annotated with @Shared so they persist across all tests in the spec.
Conclusion
The Spock Framework is one of the best things about the Groovy ecosystem. It takes the tedium out of testing and replaces it with expressive, readable specifications that document your code’s behavior as they verify it. The given/when/then structure makes tests self-documenting. Data-driven testing with where blocks eliminates copy-paste test methods. Built-in mocking, stubbing, and spying means no extra dependencies. And the power assertion output makes debugging failures fast.
From simple utility classes to complex services with multiple dependencies, the Spock Framework gives you the tools to write tests that are both thorough and a pleasure to read. If you are doing any Groovy development – from scripts to Grails applications – Spock should be your go-to testing framework.
For more on Groovy’s built-in assertion capabilities that complement Spock, check out our Groovy Assert and Power Assert guide. And for advanced testing patterns including integration tests and test fixtures, visit the full Groovy Testing Guide.
Summary
- Spock specifications extend
spock.lang.Specificationand use descriptive string method names given/when/thenprovides clear structure;expectis for simple testswhereblocks make data-driven testing elegant – use@Unrollfor individual test names- Built-in
Mock(),Stub(), andSpy()eliminate the need for Mockito - Use
thrown()for exception testing andnotThrown()for safety checks - Interaction verification with
N * mock.method()lets you verify call counts and arguments - Every boolean expression in
then/expectis automatically an assertion
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Scripting Automation – Practical Guide
Frequently Asked Questions
What is the Spock Framework in Groovy?
Spock is a testing and specification framework for Groovy and Java applications. It combines testing, mocking, and data-driven testing into a single framework. Spock specifications extend spock.lang.Specification and use expressive given/when/then blocks that read like plain English. It runs on the JUnit Platform and works with Gradle, Maven, and all major IDEs.
How is Spock different from JUnit?
Spock offers several advantages over JUnit: descriptive string test names instead of method names, implicit assertions in then/expect blocks, built-in mocking and stubbing (no Mockito needed), elegant data-driven testing with where blocks, and power assertion output that shows sub-expression values on failure. JUnit has a larger ecosystem, but the Spock Framework is more expressive for Groovy projects.
What are given, when, and then blocks in Spock?
These are Spock’s core block types that structure your test. given sets up preconditions and test fixtures. when performs the action under test (the stimulus). then contains assertions that verify the expected outcome. Every boolean expression in a then block is automatically treated as an assertion – no assert keyword needed.
How do I do data-driven testing with Spock?
Use the where block with a data table. Columns are separated by | for inputs and || for expected outputs. Each row runs the test once with those values. Add @Unroll to report each row as a separate test. You can also use data pipes (x << [1, 2, 3]) and derived variables (desc = x > 0 ? 'positive' : 'negative') for more flexibility.
Can Spock test Java code or only Groovy?
Spock can test both Java and Groovy code. Spock specifications are written in Groovy, but the classes under test can be pure Java. Many teams write their production code in Java and their tests in Groovy with Spock, getting the best of both worlds – type-safe production code with expressive, concise tests.
Interview Questions on the Spock Framework
“Interviewers rarely ask for definitions. They ask what happens in situations like these.”
Q: A teammate stubs repo.findUser(_) >> testUser in the given block, then verifies 1 * repo.findUser(_) in the then block. The count passes, but the service suddenly gets null instead of testUser. What broke?
The interaction in the then block redefined the call. In Spock, a verification like 1 * repo.findUser(_) is itself an interaction, and because it declares no response, it wins for that invocation and returns the default null. Stubbing and verification for the same call have to be combined in one line: 1 * repo.findUser(_) >> testUser. This is probably the single most common Spock bug in real codebases, and interviewers use it to separate people who have shipped Spock tests from people who have skimmed the docs.
Q: In review you see a spec where every collaborator is a Mock() and every then block verifies exact call counts on all of them. The tests break on every refactor. What would you change?
That is over-specification. The rule of thumb: use Stub() for queries, things the code reads data from, and reserve Mock() with interaction verification for commands, the side effects that are the actual point of the test, like sending an email or writing a record. When you verify the count of every incidental read, you are testing the implementation rather than the behavior, so any internal refactor fails the suite without a real bug. Trimming verification to the meaningful side effects makes the tests both shorter and more durable.
Q: A spec initializes an expensive database container in setupSpec() and stores it in a normal field. It fails immediately. Why, and what is the fix?
setupSpec() runs once before any feature method, before Spock creates the per-feature specification instances, so it can only touch fields annotated @Shared or static fields. A normal field does not exist yet at that point, which is why Spock rejects it. Marking the field @Shared fixes it, with the caveat that the object is now shared mutable state across all feature methods, so tests must not depend on it being reset. For per-test isolation, use plain fields with setup() instead.
Q: You need to verify that a notification service sends exactly one email to the right recipient, but the message object is built deep inside the method under test. How do you assert on it in Spock?
Use an argument constraint closure in the interaction: 1 * mailer.send({ it.to == 'ops@example.com' && it.subject.contains('ALERT') }). The closure receives the actual argument, and the interaction only matches when it returns true, so you get precise assertions without exposing the message construction. If the check grows complicated, capture the argument into a local variable inside the closure and assert on it afterwards. This capability, built into the mocking DSL, is a big reason Spock teams rarely reach for a separate captor API like Mockito’s.
Q: A colleague extracted assertion logic into a helper method that compares two result objects with boolean expressions, and now the feature passes even with wrong data. What happened?
Implicit assertions only apply to expressions written directly in then and expect blocks. Inside a regular helper method, those booleans are evaluated and thrown away, so the method quietly returns and the test passes. The fixes are to put explicit assert statements inside the helper, or wrap the checks in verifyAll { }, which turns every expression in its closure into an assertion and reports all failures together. Also make the helper return void so nobody mistakes its last expression for a checked value.
Related Posts
Previous in Series: Groovy Security – Sandboxing Scripts
Next in Series: Groovy Scripting Automation – Practical Guide
Related Topics You Might Like:
- Groovy Assert and Power Assert
- Groovy Testing Guide (this post)
- Groovy Closures Tutorial
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment