Groovy testing with Spock Framework. 12+ examples covering given/when/then, data-driven testing, mocking, stubbing, exception testing, @Unroll. Groovy 5.x.
“The best code is code you can prove works. Spock makes that proof readable, expressive, and even enjoyable.”
Kent Beck, Test-Driven Development
Last Updated: March 2026 | Tested on: Groovy 5.x, 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 Spock 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:
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 Spock 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, Spock 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 Spock 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.
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