一键导入
grails-tdd
Test-Driven Development workflow for Grails applications with Spock. Trigger: When implementing features, fixing bugs, or refactoring in Grails projects.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Test-Driven Development workflow for Grails applications with Spock. Trigger: When implementing features, fixing bugs, or refactoring in Grails projects.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Design or review REST APIs, define endpoints, request/response schemas, or evaluate API contracts. Use when the user is designing a new API, reviewing an existing one, or asking about HTTP conventions.
Perform a structured code review. Use when the user asks to review code, check a PR, audit a file, or validate an implementation against best practices.
Systematically investigate and fix bugs, errors, or unexpected behavior. Use when the user reports a bug, shares an error/stacktrace, or asks why something isn't working.
Write or improve technical documentation. Use when the user asks to document code, write a README, generate API docs, write a technical spec, or explain how something works for other engineers.
Improve the internal structure of code without changing its behavior. Use when the user asks to clean up, refactor, simplify, or reduce complexity in existing code.
Review code or configuration for security vulnerabilities. Use when the user asks to audit security, check for vulnerabilities, review auth logic, or validate input handling.
| name | grails-tdd |
| description | Test-Driven Development workflow for Grails applications with Spock. Trigger: When implementing features, fixing bugs, or refactoring in Grails projects. |
| license | Apache-2.0 |
| metadata | {"author":"mrp4sten","version":"1.0","scope":["root"],"auto_invoke":["Implementing features in Grails","Fixing bugs in Grails applications","Refactoring Grails code"]} |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash |
Test-Driven Development applied to Grails applications using Spock framework.
Core Principle: Write test first, make it pass, then refactor.
1. RED → Write a failing test
2. GREEN → Write minimal code to pass
3. REFACTOR → Improve code quality
4. REPEAT
Test individual classes in isolation.
Location: src/test/groovy/
Traits:
ControllerUnitTest — For controllersServiceUnitTest — For servicesDataTest — For domain classes (GORM mocking)Test with real database and Spring context.
Location: src/integration-test/groovy/
Annotations:
@Integration — Loads full Grails application@Rollback — Rolls back database changes after each test// src/test/groovy/com/example/UserSpec.groovy
package com.example
import grails.testing.gorm.DataTest
import spock.lang.Specification
class UserSpec extends Specification implements DataTest {
def setup() {
mockDomain(User)
}
void "should create user with valid data"() {
given: "valid user data"
def user = new User(
email: "john@example.com",
firstName: "John",
lastName: "Doe",
password: "secret123"
)
when: "saving the user"
user.save()
then: "user is persisted"
user.id != null
!user.hasErrors()
User.count() == 1
}
void "should fail validation when email is missing"() {
given: "user without email"
def user = new User(
firstName: "John",
lastName: "Doe",
password: "secret123"
)
when: "validating the user"
user.validate()
then: "validation fails"
user.hasErrors()
user.errors.getFieldError('email') != null
}
}
Run test:
./gradlew test --tests UserSpec
Result: ❌ FAILS (User class doesn't exist yet)
// grails-app/domain/com/example/User.groovy
package com.example
class User {
String email
String firstName
String lastName
String password
Date dateCreated
Date lastUpdated
static constraints = {
email nullable: false, blank: false, email: true, unique: true
firstName nullable: false, blank: false
lastName nullable: false, blank: false
password nullable: false, blank: false, minSize: 8
}
static mapping = {
table 'users'
version false
}
}
Run test:
./gradlew test --tests UserSpec
Result: ✅ PASSES
// src/test/groovy/com/example/UserServiceSpec.groovy
package com.example
import grails.testing.gorm.DataTest
import grails.testing.services.ServiceUnitTest
import spock.lang.Specification
class UserServiceSpec extends Specification implements ServiceUnitTest<UserService>, DataTest {
def setup() {
mockDomain(User)
}
void "should create user successfully"() {
given: "valid user data"
def dto = [
email: "john@example.com",
firstName: "John",
lastName: "Doe",
password: "secret123"
]
when: "creating user"
def user = service.create(dto)
then: "user is created"
user != null
user.id != null
user.email == "john@example.com"
User.count() == 1
}
void "should hash password before saving"() {
given: "user data with plain text password"
def dto = [
email: "john@example.com",
firstName: "John",
lastName: "Doe",
password: "secret123"
]
when: "creating user"
def user = service.create(dto)
then: "password is hashed"
user.password != "secret123"
user.password.length() > 20
}
void "should throw exception when email already exists"() {
given: "existing user"
new User(
email: "john@example.com",
firstName: "John",
lastName: "Doe",
password: "hashed"
).save(flush: true)
and: "new user with same email"
def dto = [
email: "john@example.com",
firstName: "Jane",
lastName: "Smith",
password: "secret456"
]
when: "creating user"
service.create(dto)
then: "exception is thrown"
thrown(grails.validation.ValidationException)
}
}
Run test:
./gradlew test --tests UserServiceSpec
Result: ❌ FAILS (UserService doesn't exist)
// grails-app/services/com/example/UserService.groovy
package com.example
import grails.gorm.transactions.Transactional
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
@Transactional
class UserService {
BCryptPasswordEncoder passwordEncoder
User create(Map dto) {
def user = new User(
email: dto.email,
firstName: dto.firstName,
lastName: dto.lastName,
password: passwordEncoder.encode(dto.password)
)
user.save(flush: true, failOnError: true)
return user
}
@Transactional(readOnly = true)
User get(Long id) {
User.get(id)
}
@Transactional(readOnly = true)
List<User> list(Map params) {
User.list(params)
}
void delete(Long id) {
User.get(id)?.delete(flush: true)
}
}
Configure BCrypt in grails-app/conf/spring/resources.groovy:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
beans = {
passwordEncoder(BCryptPasswordEncoder)
}
Run test:
./gradlew test --tests UserServiceSpec
Result: ✅ PASSES
// src/test/groovy/com/example/UserControllerSpec.groovy
package com.example
import grails.testing.gorm.DataTest
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification
class UserControllerSpec extends Specification
implements ControllerUnitTest<UserController>, DataTest {
def setup() {
mockDomain(User)
}
void "should return list of users"() {
given: "existing users"
new User(email: "a@test.com", firstName: "Alice", lastName: "A", password: "hash1").save()
new User(email: "b@test.com", firstName: "Bob", lastName: "B", password: "hash2").save()
when: "requesting user list"
controller.index()
then: "users are returned"
response.status == 200
response.json.size() == 2
response.json[0].email == "a@test.com"
}
void "should create user with valid data"() {
given: "valid user data"
request.json = [
email: "john@test.com",
firstName: "John",
lastName: "Doe",
password: "secret123"
]
when: "creating user"
controller.save()
then: "user is created"
response.status == 201
response.json.id != null
response.json.email == "john@test.com"
User.count() == 1
}
void "should return 400 when validation fails"() {
given: "invalid user data (missing email)"
request.json = [
firstName: "John",
lastName: "Doe",
password: "secret123"
]
when: "creating user"
controller.save()
then: "validation error is returned"
response.status == 400
response.json.errors != null
}
}
Run test:
./gradlew test --tests UserControllerSpec
Result: ❌ FAILS (UserController doesn't exist)
// grails-app/controllers/com/example/UserController.groovy
package com.example
import grails.validation.ValidationException
class UserController {
UserService userService
static responseFormats = ['json']
static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
def index() {
respond userService.list(params)
}
def show(Long id) {
respond userService.get(id)
}
def save() {
try {
def user = userService.create(request.JSON)
respond user, [status: 201]
} catch (ValidationException e) {
respond([errors: e.errors], [status: 400])
}
}
def delete(Long id) {
userService.delete(id)
render status: 204
}
}
Run test:
./gradlew test --tests UserControllerSpec
Result: ✅ PASSES
// src/integration-test/groovy/com/example/UserIntegrationSpec.groovy
package com.example
import grails.testing.mixin.integration.Integration
import grails.gorm.transactions.Rollback
import spock.lang.Specification
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
@Integration
@Rollback
class UserIntegrationSpec extends Specification {
HttpClient client
void setup() {
client = HttpClient.create(new URL("http://localhost:${serverPort}"))
}
void cleanup() {
client.close()
}
void "should create user via REST API"() {
given: "user data"
def body = [
email: "integration@test.com",
firstName: "Integration",
lastName: "Test",
password: "secret123"
]
when: "posting to /user"
def request = HttpRequest.POST("/user", body)
def response = client.toBlocking().exchange(request, Map)
then: "user is created"
response.status == HttpStatus.CREATED
response.body().id != null
response.body().email == "integration@test.com"
and: "user is persisted in database"
User.count() == 1
User.findByEmail("integration@test.com") != null
}
}
Run integration test:
./gradlew integrationTest --tests UserIntegrationSpec
// ✅ Good (Descriptive)
void "should create user when valid data is provided"() { ... }
void "should throw ValidationException when email is duplicate"() { ... }
void "should return 404 when user ID does not exist"() { ... }
// ❌ Bad (Not descriptive)
void "testCreateUser"() { ... }
void "test1"() { ... }
| Scenario | Test Type | Why |
|---|---|---|
| Validate domain constraints | Unit Test | Fast, no database |
| Test service business logic | Unit Test | Mock dependencies |
| Test controller JSON response | Unit Test | Mock service |
| Test database queries | Integration Test | Real database needed |
| Test full API flow | Integration Test | End-to-end validation |
class UserServiceSpec extends Specification implements ServiceUnitTest<UserService> {
void "should send welcome email"() {
given: "mocked email service"
def emailService = Mock(EmailService)
service.emailService = emailService
when:
service.create([email: "john@test.com", ...])
then:
1 * emailService.sendWelcomeEmail("john@test.com")
}
}
void "should handle null email gracefully"() { ... }
void "should handle very long names (> 255 chars)"() { ... }
void "should handle SQL injection in search"() { ... }
void "should handle concurrent user creation"() { ... }
// ❌ Bad (Tests depend on each other)
void "test1 creates user"() {
user = new User(...).save()
}
void "test2 updates user"() {
user.firstName = "Updated" // Depends on test1
}
// ✅ Good (Each test is independent)
void "should create user"() {
def user = new User(...).save()
assert user.id != null
}
void "should update user"() {
def user = new User(...).save() // Creates own fixture
user.firstName = "Updated"
}
./gradlew test
./gradlew test --tests UserServiceSpec
./gradlew test --tests UserServiceSpec."should create user successfully"
./gradlew integrationTest
./gradlew test --continuous
After running tests, view report at:
build/reports/tests/test/index.html
Add to build.gradle:
plugins {
id 'jacoco'
}
jacoco {
toolVersion = "0.8.8"
}
test {
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test
reports {
xml.enabled true
html.enabled true
}
}
Generate coverage report:
./gradlew test jacocoTestReport
View report:
build/reports/jacoco/test/html/index.html
void "should find users by email domain"() {
given: "users with different email domains"
new User(email: "alice@gmail.com", ...).save()
new User(email: "bob@gmail.com", ...).save()
new User(email: "charlie@yahoo.com", ...).save()
when: "finding Gmail users"
def users = User.findAllByEmailIlike("%@gmail.com")
then:
users.size() == 2
users*.email == ["alice@gmail.com", "bob@gmail.com"]
}
void "should rollback on error"() {
given:
def user = new User(...).save(flush: true)
when: "update fails"
user.email = null // Invalid
user.save(flush: true, failOnError: true)
then:
thrown(ValidationException)
and: "transaction is rolled back"
User.get(user.id).email != null // Original value restored
}
void "should process async task"() {
given:
def latch = new CountDownLatch(1)
def result
when:
service.processAsync { data ->
result = data
latch.countDown()
}
then:
latch.await(5, TimeUnit.SECONDS)
result != null
}
// Bad
void "should call repository.save()"() {
then:
1 * repository.save(_) // Don't test internal calls
}
// Good
void "should persist user"() {
when:
service.create(dto)
then:
User.count() == 1 // Test outcome, not implementation
}
// Bad
def user = Mock(User)
def service = Mock(UserService)
// Testing mocks, not real code!
// Good
def user = new User(...) // Use real objects when possible
// Bad
void "should set email"() {
user.setEmail("test@test.com")
assert user.getEmail() == "test@test.com"
}
// These are auto-generated, no need to test!