Spock with Spring Boot: Integration Testing Setup and @SpringBean

Spock with Spring Boot: Integration Testing Setup and @SpringBean

Spock integrates with Spring Boot via the spock-spring module, which brings @SpringBootTest, @SpringBean, and @SpringSpy into Spock specs. You get the full Spring context with Spock's readable BDD syntax, mock injection via @SpringBean, and real bean overrides via @SpringSpy. This guide covers setup, common patterns, and how to test REST endpoints with MockMvc.

Setup: Adding Spock to a Spring Boot Project

Add to build.gradle:

dependencies {
    testImplementation 'org.spockframework:spock-core:2.4-groovy-4.0'
    testImplementation 'org.spockframework:spock-spring:2.4-groovy-4.0'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // For Groovy compilation
    testImplementation 'org.apache.groovy:groovy:4.0.21'
}

Spock specs must live in src/test/groovy/. Spring Boot's test autoconfiguration picks them up automatically.

@SpringBootTest — Full Context Integration Tests

The simplest integration test loads the full application context:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiSpec extends Specification {

    @Autowired
    TestRestTemplate restTemplate

    def "GET /users returns 200 and list"() {
        when:
        def response = restTemplate.getForEntity("/users", List)

        then:
        response.statusCode == HttpStatus.OK
        response.body.size() >= 0
    }
}

WebEnvironment.RANDOM_PORT starts a real embedded server. WebEnvironment.MOCK (the default) uses a mock servlet environment — faster but doesn't exercise filters and servlet containers.

Application Properties for Tests

Override properties in test specs:

@SpringBootTest
@TestPropertySource(properties = [
    "spring.datasource.url=jdbc:h2:mem:testdb",
    "feature.flag.new-ui=true"
])
class FeatureFlagSpec extends Specification { ... }

Or create src/test/resources/application-test.yaml and activate the profile:

@SpringBootTest
@ActiveProfiles("test")
class ServiceSpec extends Specification { ... }

@SpringBean — Injecting Mocks into the Spring Context

@SpringBean is the Spock equivalent of Spring's @MockBean. It creates a Spock mock and registers it as a Spring bean, replacing any existing bean of that type:

@SpringBootTest
class OrderServiceSpec extends Specification {

    @SpringBean
    PaymentGateway paymentGateway = Mock()

    @SpringBean
    EmailService emailService = Stub()

    @Autowired
    OrderService orderService

    def "order placement calls payment gateway"() {
        given:
        def order = new Order(total: 99.99)

        when:
        orderService.place(order)

        then:
        1 * paymentGateway.charge(99.99, _)
    }
}

The @SpringBean field must be initialized with Mock(), Stub(), or Spy() in the field declaration — not in setup().

Why @SpringBean instead of field injection + Mock()? If you just declare def paymentGateway = Mock(PaymentGateway) without @SpringBean, Spring injects the real bean into orderService. Your mock exists but isn't what the service uses. @SpringBean replaces the Spring context's bean with your mock.

@SpringBean for Interfaces and Final Classes

For interfaces, Spock creates a JDK proxy. For concrete classes, it uses CGLIB subclassing. If you're mocking a final class, add the spock-mock-maker-inline dependency:

testImplementation 'org.spockframework:spock-mock-maker-inline:2.4-groovy-4.0'

Then declare in src/test/resources/org/mockito/plugins/MockMaker:

mock-maker-inline

This uses ByteBuddy to create mocks of final classes.

@SpringSpy — Spying on Real Beans

@SpringSpy is the equivalent of Spring's @SpyBean. It wraps the real Spring bean with a Spock spy:

@SpringBootTest
class AuditSpec extends Specification {

    @SpringSpy
    AuditService auditService

    @Autowired
    UserService userService

    def "user creation is audited"() {
        when:
        userService.createUser("alice", "alice@example.com")

        then:
        1 * auditService.log(AuditEvent.USER_CREATED, _)
    }
}

The real auditService bean is called — but Spock records the interactions. You can verify calls happened AND let the real implementation run.

To override specific methods:

@SpringSpy
AuditService auditService = {
    isAuditEnabled() >> false
}

Testing REST Controllers with MockMvc

For controller-layer tests without a full server, use @WebMvcTest with MockMvc:

@WebMvcTest(UserController)
class UserControllerSpec extends Specification {

    @Autowired
    MockMvc mockMvc

    @SpringBean
    UserService userService = Mock()

    def "GET /users/:id returns 200 with user data"() {
        given:
        userService.findById(42L) >> new User(id: 42, name: "Alice")

        when:
        def result = mockMvc.perform(get("/users/42"))
                            .andReturn()

        then:
        result.response.status == 200
        result.response.contentAsString.contains("Alice")
    }

    def "GET /users/:id returns 404 when not found"() {
        given:
        userService.findById(99L) >> null

        when:
        def result = mockMvc.perform(get("/users/99"))
                            .andReturn()

        then:
        result.response.status == 404
    }
}

Import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* for the get(), post(), put(), delete() helpers.

Testing POST Endpoints with JSON Body

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.http.MediaType

def "POST /users creates user and returns 201"() {
    given:
    def requestBody = '{"name": "Bob", "email": "bob@example.com"}'
    userService.create(_ as CreateUserRequest) >> new User(id: 1, name: "Bob")

    when:
    def result = mockMvc.perform(
        post("/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content(requestBody)
    ).andReturn()

    then:
    result.response.status == 201
    1 * userService.create({ it.name == "Bob" })
}

Context Caching

Spring Boot caches the application context between tests to avoid reloading it for every spec. Tests that use @SpringBean or @SpringSpy create a modified context and may not share the cache with tests that don't.

To maximize cache reuse:

  1. Put common @SpringBean declarations in a base spec class
  2. Use @TestConfiguration beans for shared test infrastructure
  3. Avoid @DirtiesContext unless necessary — it forces a full context reload
// Base class with common mocks
@SpringBootTest
abstract class BaseIntegrationSpec extends Specification {
    @SpringBean
    EmailService emailService = Stub()

    @SpringBean
    NotificationService notificationService = Stub()
}

class OrderSpec extends BaseIntegrationSpec {
    // Inherits the stubs — shares context with other BaseIntegrationSpec subclasses
    @Autowired
    OrderService orderService
}

Transactions in Integration Tests

Use @Transactional to roll back database changes after each test:

@SpringBootTest
@Transactional
class UserRepositorySpec extends Specification {

    @Autowired
    UserRepository userRepository

    def "find by email returns user"() {
        given:
        userRepository.save(new User(email: "alice@example.com"))

        when:
        def found = userRepository.findByEmail("alice@example.com")

        then:
        found.present
        found.get().email == "alice@example.com"
    }
    // transaction rolled back — DB is clean for next test
}

Note: @Transactional doesn't work with WebEnvironment.RANDOM_PORT — the test runs in a different transaction than the server. For those cases, manually clean up in cleanup() or use @DirtiesContext.

Data-Driven Integration Tests

Combine Spring context with Spock's where: block for parameterized integration tests:

@SpringBootTest
class ValidationSpec extends Specification {

    @Autowired
    UserService userService

    @Unroll
    def "invalid email '#email' is rejected"() {
        when:
        def result = userService.validateEmail(email)

        then:
        !result.valid
        result.message == expectedError

        where:
        email              | expectedError
        ""                 | "Email is required"
        "notanemail"       | "Invalid email format"
        "missing@tld"      | "Invalid domain"
        "@nodomain.com"    | "Missing local part"
    }
}

The Spring context loads once and is reused for all four iterations.

Common Pitfalls

@SpringBean not initialized in field declaration. You cannot do:

@SpringBean
PaymentGateway paymentGateway  // ERROR: must be initialized

// Correct:
@SpringBean
PaymentGateway paymentGateway = Mock()

@Autowired with @SpringBean on the same field. You inject with @Autowired, mock with @SpringBean — they're different fields for different purposes.

Missing spock-spring dependency. Without it, @SpringBean is not recognized and acts as a plain field annotation — tests appear to work but use the real bean.

Context pollution across specs. If one spec modifies a @Shared bean's state, other specs see it. Use @DirtiesContext on the offending spec, or reset the state in cleanupSpec().

Summary

Spock integrates with Spring Boot through spock-spring, giving you @SpringBootTest context loading, @SpringBean for mock injection, and @SpringSpy for partial bean overrides. You write the same readable given/when/then specs — the Spring context is just another collaborator. Use @WebMvcTest for controller slices, @Transactional for DB rollback, and base spec classes to share context across tests. The result is integration tests that are as readable as unit tests, with the full power of the Spring context behind them.

Read more

Start now free