Introduction to Spock
Spock is a powerful testing and specification framework for Java and Groovy applications. Combining the expressiveness of Groovy with a behavior-driven development (BDD) style, Spock lets developers write tests that read almost like plain English. Whether you are testing simple Java classes, Spring Boot services, or complex enterprise systems, Spock offers a clean syntax, rich assertion capabilities, and built-in mocking that make it a compelling alternative to JUnit.
What Makes Spock Different?
Unlike traditional JUnit tests, Spock specifications are written in Groovy and organized around the concept of specifications rather than test cases. Each specification describes the behavior of a class or feature using labeled blocks such as given, when, then. This structure makes tests self-documenting and easier to maintain.
Why Spock Matters
- Readable syntax: BDD-style blocks make intent obvious.
- Powerful assertions: Groovy's truthiness and Spock's condition handling produce detailed failure messages automatically.
- Data-driven testing: Built-in
whereblocks eliminate boilerplate for parameterized tests. - Integrated mocking: No need for Mockito or PowerMock; Spock has its own mocking framework.
- Java interoperability: Tests written in Groovy can test plain Java code seamlessly.
- Rich reporting: Failure messages include the exact expression and values involved.
Setting Up Spock
To use Spock in a Gradle project, add the Groovy plugin and the Spock dependency. Spock 2.x is based on JUnit 5 Platform and requires Java 8 or higher.
plugins {
id 'java'
id 'groovy'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0'
testImplementation 'org.apache.groovy:groovy:4.0.15'
}
test {
useJUnitPlatform()
}
For Maven users, include the Groovy compiler plugin and the Spock dependency:
<dependencies>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>2.3-groovy-4.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Anatomy of a Spock Specification
Every Spock test class extends spock.lang.Specification. Inside, each test method is called a feature method and uses labeled blocks to structure the test.
import spock.lang.Specification
class CalculatorSpec extends Specification {
def "should add two numbers correctly"() {
given: "a calculator instance"
def calculator = new Calculator()
when: "two numbers are added"
def result = calculator.add(2, 3)
then: "the result is the sum"
result == 5
}
}
Core Blocks Explained
given(orsetup): Prepares the test fixtures.when: Triggers the action under test.then: Contains assertions and exception expectations.expect: Combineswhenandthenfor simpler tests.cleanup: Runs after each feature method to release resources.where: Provides data tables for parameterized tests.
Assertions in Spock
Spock does not require explicit assert keywords. Any boolean expression in a then block is automatically treated as an assertion. When an assertion fails, Spock prints a detailed breakdown of the values involved.
def "list should contain expected elements"() {
given:
def list = [1, 2, 3]
expect:
list.size() == 3
list.contains(2)
list.sum() == 6
}
If list.sum() returned the wrong value, Spock would output something like:
Condition not satisfied:
list.sum() == 6
| | |
| 5 false
[1, 2, 3]
Data-Driven Testing with where
One of Spock's standout features is the where block, which allows you to run the same test logic against multiple data sets using clean data tables.
import spock.lang.Specification
import spock.lang.Unroll
class MathSpec extends Specification {
@Unroll
def "multiplying #a by #b should return #expected"() {
expect:
a * b == expected
where:
a | b || expected
1 | 2 || 2
3 | 4 || 12
0 | 5 || 0
-2| 3 || -6
}
}
The @Unroll annotation generates a separate test report entry for each row, replacing the placeholders (#a, #b, #expected) with actual values. This makes test reports far more informative.
Using Data Pipes
Instead of data tables, you can also use data pipes to feed values from lists or computed sources:
def "squares should be computed correctly"() {
expect:
n * n == square
where:
n << [1, 2, 3, 4, 5]
square << [1, 4, 9, 16, 25]
}
Mocking and Stubbing
Spock provides a built-in mocking framework. You can create mocks with Mock(), stubs with Stub(), and spies with Spy(). Interactions are verified in the then block using a natural syntax.
import spock.lang.Specification
class OrderServiceSpec extends Specification {
def "should call payment gateway on checkout"() {
given: "a mocked payment gateway"
def paymentGateway = Mock(PaymentGateway)
def service = new OrderService(paymentGateway)
when: "an order is checked out"
service.checkout(new Order(amount: 100))
then: "the payment gateway is invoked once with the correct amount"
1 * paymentGateway.charge(100)
}
}
Common Interaction Patterns
1 * mock.method(args)โ exactly one call.0 * mock._โ no other interactions on the mock.3 * mock.method(_)โ three calls with any argument._ * mock.method(args)โ any number of calls.mock.method(_) >> "result"โ stub a return value.mock.method(_) >> { throw new RuntimeException() }โ stub an exception.
def "stub should return configured values"() {
given:
def repository = Stub(UserRepository)
repository.findById(1) >> new User(id: 1, name: "Alice")
repository.findById(_) >> null
expect:
repository.findById(1).name == "Alice"
repository.findById(99) == null
}
Testing Exceptions
Spock makes exception testing elegant. In a then block, you can declare a thrown exception type and inspect its properties.
def "should throw exception when dividing by zero"() {
given:
def calculator = new Calculator()
when:
calculator.divide(10, 0)
then:
def e = thrown(ArithmeticException)
e.message == "Cannot divide by zero"
}
Setup and Cleanup Methods
Spock provides lifecycle methods that run before and after feature methods or the entire specification.
import spock.lang.Specification
class LifecycleSpec extends Specification {
def setupSpec() {
println "Runs once before all feature methods"
}
def setup() {
println "Runs before each feature method"
}
def cleanup() {
println "Runs after each feature method"
}
def cleanupSpec() {
println "Runs once after all feature methods"
}
def "example feature"() {
expect:
true
}
}
Using Spock with Spring Boot
Spock integrates smoothly with Spring Boot through the spock-spring module. Add the dependency and annotate your specification with @SpringBootTest.
dependencies {
testImplementation 'org.spockframework:spock-spring:2.3-groovy-4.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class UserServiceIntegrationSpec extends Specification {
@Autowired
UserService userService
def "should find a user by id"() {
when:
def user = userService.findById(1L)
then:
user != null
user.name == "Alice"
}
}
Best Practices
1. Write Descriptive Feature Names
Use string literals for feature method names that describe behavior in plain English. This improves test reports and documentation.
def "should reject order when stock is insufficient"() { ... }
2. Keep Tests Focused
Each feature method should test one behavior. Avoid combining multiple unrelated assertions in a single test, as this makes failures harder to diagnose.
3. Use @Unroll for Data-Driven Tests
Always annotate parameterized tests with @Unroll so each data row appears separately in reports, making it obvious which input failed.
4. Prefer Stubs Over Mocks When Possible
Use Stub() when you only need canned responses and Mock() when you need to verify interactions. Overusing mocks can lead to brittle tests tightly coupled to implementation details.
5. Avoid Logic in Tests
Tests should not contain loops, conditionals, or complex computations. If you find yourself writing logic, refactor the production code or use data tables.
6. Leverage Helper Methods
Extract repetitive setup into helper methods within the specification to keep feature methods concise and readable.
def validOrder() {
new Order(amount: 50, items: ["book", "pen"])
}
def "should process valid order"() {
given:
def order = validOrder()
expect:
order.isValid()
}
7. Verify No Unexpected Interactions
End your then block with 0 * _ to catch unexpected calls on mocks, ensuring your code only triggers the collaborators you expect.
Conclusion
Spock brings clarity, power, and elegance to the testing experience for Java and Groovy developers. Its BDD-style blocks make tests read like specifications, the where block removes the friction from data-driven testing, and the integrated mocking framework eliminates the need for additional libraries. By adopting Spock and following best practices such as descriptive naming, focused feature methods, and judicious use of mocks and stubs, teams can build a robust, maintainable test suite that doubles as living documentation. Whether you are starting a new project or migrating from JUnit, Spock is a framework worth investing in for cleaner, more expressive, and more reliable tests.