skills/spock/SKILL.md
Use when writing or refactoring Spock tests in Java projects - enforces data-driven testing with where blocks, proper mock/stub placement, and descriptive test names following Spock best practices
npx skillsauth add mbarbieri/my-claude spockInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Write maintainable Spock tests using data-driven testing, proper block structure, and clear naming. Core principle: Similar tests with different inputs = one parameterized test with where: block.
Writing 3+ similar tests = You MUST use where: block
No exceptions:
Why: Refactoring to where: takes 2 minutes. Maintaining 10 separate tests takes hours.
You're about to violate the Iron Rule if:
All of these mean: Use where: block NOW.
def "should calculate 20% discount for premium"() {
when:
def result = calculator.calculateDiscount(new BigDecimal("100"), CustomerType.PREMIUM)
then:
result == new BigDecimal("20.00")
}
def "should calculate 10% discount for regular"() {
when:
def result = calculator.calculateDiscount(new BigDecimal("100"), CustomerType.REGULAR)
then:
result == new BigDecimal("10.00")
}
def "should calculate 5% discount for new"() {
when:
def result = calculator.calculateDiscount(new BigDecimal("100"), CustomerType.NEW)
then:
result == new BigDecimal("5.00")
}
def "should calculate no discount for guest"() {
when:
def result = calculator.calculateDiscount(new BigDecimal("100"), CustomerType.GUEST)
then:
result == BigDecimal.ZERO
}
Problems: 4 test methods, 20+ lines, duplicated structure, hard to see pattern
def "should calculate #expectedDiscount discount for #customerType customer"() {
expect:
calculator.calculateDiscount(orderAmount, customerType) == expectedDiscount
where:
customerType | orderAmount | expectedDiscount
CustomerType.PREMIUM | new BigDecimal(100) | new BigDecimal("20.00")
CustomerType.REGULAR | new BigDecimal(100) | new BigDecimal("10.00")
CustomerType.NEW | new BigDecimal(100) | new BigDecimal("5.00")
CustomerType.GUEST | new BigDecimal(100) | BigDecimal.ZERO
}
Benefits: 1 test method, 10 lines, pattern obvious, easy to add cases
| Block | Purpose | Example |
|-------|---------|---------|
| given: | Setup, stubs | repository.findById(1) >> Optional.of(user) |
| when: | Execute action | service.processOrder(orderId) |
| then: | Assertions, mock verification | 1 * service.save(_) |
| expect: | Single-line assertion | calculator.add(2, 3) == 5 |
| where: | Data table for parameters | a \| b \| sum |
Stub → Return fake data → Goes in given: → Use >>
given:
repository.findById(1) >> Optional.of(user) // Stub
Mock → Verify interaction → Goes in then: → Use *
then:
1 * emailService.sendWelcome(user) // Mock verification
where:
columnA | columnB | expected
value1 | value2 | result1
value3 | value4 | result2
Use #variable in test names to show which parameter is tested:
def "should validate #email as #validity"() {
expect:
validator.isValid(email) == isValid
where:
email | validity | isValid
"[email protected]" | "valid" | true
"invalid" | "invalid"| false
}
| Mistake | Fix |
|---------|-----|
| Writing 3+ similar tests | Use where: block |
| Stub in then: block | Move to given: |
| Mock verification in given: | Move to then: |
| Test name: testCalculate() | Use full sentence: "should calculate discount for premium customer" |
| Hardcoded timestamps | Use LocalDateTime.of(2025, 1, 15, 10, 30) |
| Magic numbers | Use named variables or data table columns |
where: blocks for variationsdef "should reject invalid email: #reason"() {
expect:
!validator.isValid(email)
where:
email | reason
null | "null"
"" | "empty"
"no-at-sign" | "missing @"
"a" * 255 + "@x" | "too long"
}
def "should accept valid email: #email"() {
expect:
validator.isValid(email)
where:
email << ["[email protected]", "[email protected]", "[email protected]"]
}
| Excuse | Reality |
|--------|---------|
| "Copy-paste is faster" | Refactoring takes 2 min, maintaining duplicates takes hours |
| "I'll consolidate later" | Later never comes, duplication stays |
| "These are slightly different" | Different inputs = perfect for where: block |
| "I'm under time pressure" | Bad tests slow you down more than writing good ones |
| "Each test is simple" | Simple + duplicated = maintenance nightmare |
| "I need more coverage" | 10 separate tests ≠ better than 1 parameterized test |
Before writing a test, check:
If ANY are true → Use where: block
Before data-driven testing:
After data-driven testing:
Data-driven testing isn't optional. It's the difference between maintainable and unmaintainable test suites.
tools
Use when user provides Jira issue URLs or mentions Jira tickets - fetches issue details and comments from Jira Cloud using local jira tool, outputs AI-optimized markdown for context gathering
development
Use when writing, modifying, or reviewing Java code - applies SOLID principles, clean code practices, minimal documentation, and pragmatic abstraction to create maintainable Java applications
development
Use when the user asks to implement a feature, add a class or method, fix a bug, refactor code, add test coverage, or run autonomously to drive work forward. Supports explicit phase selection via the first argument (red | green | refactor | forever) and infers the phase from conversation and test state when no phase is given. With no arguments at all, defaults to forever (autonomous loop). Do NOT use for code review, CI/CD setup, testing questions, infrastructure, or documentation tasks.
testing
Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".