Spock Interaction-Based Testing: Mock(), Stub(), Spy() Explained

Spock Interaction-Based Testing: Mock(), Stub(), Spy() Explained

Spock's interaction-based testing uses three collaborator types: Mock() for verifying interactions, Stub() for providing canned responses without verification, and Spy() for partially mocking real objects. Interactions are declared in the then: block and verified automatically — no verify() call needed. This guide covers all three with cardinality expressions and argument matchers.

The Interaction Testing Model

Interaction-based testing verifies that your code sends the right messages to its collaborators — not just that it produces the right output. A payment service should call the fraud detector before charging the card. A cache layer should not call the database when the cache hits. These are behavioral contracts, not output assertions.

Spock's DSL makes these contracts readable:

def "fraud check is called before charging"() {
    given:
    def fraudDetector = Mock(FraudDetector)
    def charger = Mock(PaymentGateway)
    def service = new PaymentService(fraudDetector, charger)

    when:
    service.processPayment(order)

    then:
    1 * fraudDetector.check(order)
    1 * charger.charge(order.amount)
}

No @Mock annotations, no verify() calls, no MockitoAnnotations.openMocks(). The interactions are declared, and Spock verifies them when the then: block completes.

Mock() — Verify Interactions

Use Mock() when you care about how your code interacts with a dependency:

def EmailService = Mock(EmailService)

Mocks in Spock return default values by default (null for objects, 0 for numbers, false for booleans). You can also stub return values inline with interactions:

then:
1 * emailService.send(_ as Email) >> "message-id-123"

The >> operator stubs the return value. The _ is a wildcard matcher.

Cardinality

Cardinality tells Spock how many times an interaction should happen:

then:
1 * service.method()        // exactly once
0 * service.method()        // never called
2 * service.method()        // exactly twice
(1..3) * service.method()   // between 1 and 3 times
(0..1) * service.method()   // zero or one time (optional)
_ * service.method()        // any number of times (don't care)

If the actual invocation count doesn't match, Spock fails the test with a clear message:

Too few invocations for:
1 * emailService.send(_)   (0 invocations)

Strict Mock Behavior

By default, unexpected method calls on a Mock() cause test failure. This is the "strict" mock mode — if your code calls emailService.sendBCC() but your interaction only declared emailService.send(), the test fails.

To allow unexpected calls, use Stub() instead.

Stub() — Canned Responses Without Verification

Use Stub() when you need a dependency to return specific values but don't care how many times it's called:

def "service uses cached exchange rate"() {
    given:
    def rateService = Stub(ExchangeRateService)
    rateService.getRate("USD", "EUR") >> 0.92

    def converter = new CurrencyConverter(rateService)

    expect:
    converter.convert(100, "USD", "EUR") == 92.0
}

Stubs don't verify call counts. If getRate is called 3 times or not at all, the test still passes. This is what you want for setup infrastructure — cache services, config readers, feature flags.

Stubbing Multiple Calls Differently

Return different values on successive calls with >>>:

given:
def tokenService = Stub(TokenService)
tokenService.getToken() >>> ["expired-token", "valid-token"]

First call returns "expired-token", second returns "valid-token". This is useful for testing retry logic.

Or use a closure for dynamic responses:

tokenService.getToken() >> { callCount++ > 0 ? "valid" : "expired" }

Throwing Exceptions from Stubs

given:
def db = Stub(Database)
db.query(_) >> { throw new ConnectionException("timeout") }

Or for the first call only:

db.query(_) >>> [{ throw new ConnectionException() }, "result"]

Spy() — Partial Mocking of Real Objects

Use Spy() when you want to use a real object but override specific methods or verify interactions on it:

def "audit log records all write operations"() {
    given:
    def realService = new UserService(db)
    def spiedService = Spy(realService)

    when:
    spiedService.createUser(newUser)

    then:
    1 * spiedService.writeAuditLog(_)
}

The spy delegates all calls to the real object. When writeAuditLog is called, both the real implementation runs AND the interaction is recorded.

Overriding Specific Methods on a Spy

given:
def service = Spy(PaymentService) {
    isTestEnvironment() >> true
}

Only isTestEnvironment() is overridden — all other methods use the real implementation. This is cleaner than subclassing just to override one method.

Warning: Spy() requires the class to be non-final and have a no-arg constructor (or use the constructor-argument version). For final classes, use a wrapper interface.

Argument Matchers

Exact matching

1 * service.find("alice@example.com")

Wildcard: any single argument

1 * service.find(_)

Type constraint

1 * service.find(_ as String)
1 * service.save(_ as User)

Custom constraint with closure

1 * service.save({ User u -> u.email.endsWith("@company.com") })

The closure receives the actual argument and must return a boolean. If it returns false, the interaction doesn't match.

Named argument matchers (for Maps and named params)

1 * service.create([name: "Alice", role: _ as Role])

Combining matchers

1 * emailService.send(
    { it.to == "alice@example.com" },
    { it.subject.startsWith("[ALERT]") }
)

Interaction Ordering

By default, Spock interactions are unordered — the calls can happen in any sequence. For ordered verification, use then: blocks chained with and: and rely on ordering within the then: block itself. Actually, to enforce strict ordering, wrap in then: blocks:

when:
service.process(request)

then:
1 * validator.validate(request)

then:
1 * processor.execute(request)

then:
1 * notifier.notify(_)

Each then: block's interactions must complete before the next block's interactions begin.

Mixing Stubbing and Interaction Verification

You can stub and verify on the same mock:

given:
def repo = Mock(UserRepository)

when:
service.activate(userId)

then:
1 * repo.findById(userId) >> Optional.of(inactiveUser)
1 * repo.save({ User u -> u.active == true })

The >> Optional.of(inactiveUser) stubs the return value while 1 * verifies the call happened exactly once. Both in one expression.

Common Mistakes

Mistake 1: Stubbing in then: instead of given:. Stubs that don't involve interaction verification belong in given:. Putting stub.method() >> value in then: works syntactically but reads as an interaction requirement with a return value — confusing to readers.

Mistake 2: Using Mock() when you want Stub(). If your test doesn't care about call counts, use Stub(). A Mock() with _ * service.method() is just a verbose Stub().

Mistake 3: Verifying internals with Spy(). If you're spying on a class just to verify a private helper was called, you're testing implementation, not behavior. Test the observable outcome instead.

Mistake 4: Argument matcher mixing. You can't mix exact arguments and wildcards without care:

// WRONG: mixing exact and wildcard without explicit matcher
1 * service.find("alice", _)  // first arg is exact, second is wildcard — valid
1 * service.find("alice", 42) // both exact — valid
// If you use a closure matcher for one arg, Spock may require matchers for all

Full Example: Order Processing Service

class OrderServiceSpec extends Specification {

    def inventoryService = Mock(InventoryService)
    def paymentGateway = Mock(PaymentGateway)
    def emailService = Stub(EmailService)
    def orderService = new OrderService(inventoryService, paymentGateway, emailService)

    def setup() {
        emailService.send(_) >> "sent"
    }

    def "successful order reserves inventory and charges payment"() {
        given:
        def order = new Order(items: [new Item("SKU-1", 2)], total: 49.99)
        inventoryService.reserve(_ as Item) >> true

        when:
        def result = orderService.place(order)

        then:
        1 * inventoryService.reserve({ it.sku == "SKU-1" && it.quantity == 2 }) >> true
        1 * paymentGateway.charge(49.99, _ as PaymentToken)
        result.status == OrderStatus.CONFIRMED
    }

    def "out-of-stock item cancels order without charging"() {
        given:
        def order = new Order(items: [new Item("SKU-999", 1)], total: 9.99)

        when:
        def result = orderService.place(order)

        then:
        1 * inventoryService.reserve(_) >> false
        0 * paymentGateway.charge(_, _)
        result.status == OrderStatus.CANCELLED
    }
}

Summary

Spock's Mock(), Stub(), and Spy() cover the three interaction testing scenarios cleanly. Use Mock() when call verification matters, Stub() for dependencies that just need to return values, and Spy() for partial overrides on real objects. Cardinality and argument matchers let you write precise behavioral contracts without boilerplate. The interactions in then: blocks are both documentation and assertion — making your test specifications genuinely readable.

Read more

Start now free