Groovy Spock Testing – Getting Started with 12+ Tested Examples

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.

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 where blocks and @Unroll for 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 specificationsgiven/when/then blocks make tests self-documenting
  • Powerful assertions – every boolean expression in a then block 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 where block 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:

Full test

Simple assertion

Data-driven

Additional setup

Data-Driven Testing

where:
Provides test data

@Unroll
Generates separate test per row

given/when/then Pattern

given: ‘preconditions’
Setup test data and objects

when: ‘action’
Perform the operation under test

then: ‘verification’
Every expression is an assertion

Feature Method
def ‘descriptive name'()

Block Type

given: → when: → then:

expect:

where:

and:

Spock – Feature Method Block Structure

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():

Specification Class
Loaded

setupSpec()
runs ONCE before all tests
(@Shared fields only)

setup()
before each test

Feature Method 1
given → when → then

cleanup()
after each test

setup()
before each test

Feature Method 2
given → when → then

cleanup()
after each test

… more feature methods …

cleanupSpec()
runs ONCE after all tests
(@Shared fields only)

Specification
Complete

Spock Framework – Specification Lifecycle

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:

FeatureJUnit 5Spock
Test namesMethod names (camelCase)Descriptive strings with spaces
AssertionsassertEquals(), assertTrue()Plain boolean expressions
Parameterized tests@CsvSource, @MethodSourcewhere block data tables
MockingRequires MockitoBuilt-in Mock(), Stub(), Spy()
Exception testingassertThrows()thrown() in then block
Failure messagesBasicPower assertions with sub-expression values
StructureFlatgiven/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/then for state-based tests and expect for simple functional tests
  • Add label descriptions to blocks (e.g., given: "a valid user") for documentation
  • Prefer Stub() over Mock() when you do not need interaction verification
  • Use @Unroll with parameterized method names to get clear test reports
  • Keep each feature method focused on a single behavior
  • Use @Shared for expensive resources that can be safely shared across tests

DON’T:

  • Put logic in then blocks – 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 – use def setupSpec() with @Shared instead
  • 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.Specification and use descriptive string method names
  • given/when/then provides clear structure; expect is for simple tests
  • where blocks make data-driven testing elegant – use @Unroll for individual test names
  • Built-in Mock(), Stub(), and Spy() eliminate the need for Mockito
  • Use thrown() for exception testing and notThrown() for safety checks
  • Interaction verification with N * mock.method() lets you verify call counts and arguments
  • Every boolean expression in then/expect is 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.

Previous in Series: Groovy Security – Sandboxing Scripts

Next in Series: Groovy Scripting Automation – Practical Guide

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *