| name | goravel-crud-test |
| description | Generate and fix comprehensive CRUD tests for a Goravel entity. Covers all CRUD operations, sorting, pagination, search, and permissions. |
| argument-hint | [EntityName] |
| allowed-tools | Bash, Read, Write, Edit, Grep, Glob |
Goravel CRUD Test Generator
Generate tests for $ARGUMENTS.
Step 1: Generate Test Template
go run . artisan make:crud-test --controller=$ARGUMENTS
This creates tests/feature/crud/<entity>_crud_test.go.
Step 2: Fix Generated Test (CRITICAL)
The generator produces a template with known issues. Fix in this order:
Fix 1: Permission Names
Slug: fmt.Sprintf("entitycontrollers_%s", perm)
Slug: fmt.Sprintf("entity_%s", perm)
Check app/auth/permission_constants.go for the exact ServiceRegistry value.
Fix 2: API Endpoints
s.makeRequest("POST", "/api/entitycontrollers", data)
s.makeRequest("POST", "/api/entity-names", data)
Fix 3: Table Names in Cleanup
orm.Query().Exec("DELETE FROM entitycontrollers")
orm.Query().Exec("DELETE FROM entity_names")
Fix 4: Add Valid Test Data (snake_case Keys)
API request payloads MUST use snake_case keys (matching request struct form/json tags):
func (s *TestSuite) TestCreateEntity() {
data := map[string]interface{}{
"first_name": "Test",
"last_name": "Entity",
"description": "A test",
"status": "ACTIVE",
"birth_date": "1990-01-15",
"tags": []string{"tag1"},
}
resp, result := s.makeRequest("POST", "/api/entities", data)
s.Equal(http.StatusCreated, resp.StatusCode)
}
API responses return camelCase (from model json tags), so assertions use camelCase:
data := result["data"].(map[string]interface{})
s.Equal("Test", data["firstName"])
Fix 5: Initialize ALL Required Fields for ORM-Created Models
entity := &models.Entity{
Title: "Test",
Description: "Test",
Tags: []string{},
CreatedBy: &createdByInt,
}
Common error: NOT NULL constraint failed: table.array_field
Fix: Always initialize JSON array fields to []string{} or []int{}.
Fix 6: carbon.DateTime in Tests
date := *carbon.NewDateTime(carbon.Parse("2025-12-01"))
entity := &models.Entity{
Date: date,
}
Fix 7: Foreign Key Dependencies
If entity has foreign keys, create parents in setup:
func (s *TestSuite) SetupTest() {
s.RefreshDatabase()
s.setupTestUser()
parent := &models.Parent{Name: "Test Parent"}
s.Nil(facades.Orm().Query().Create(parent))
s.testParent = parent
}
Step 3: Unauthenticated Request Tests
Fresh HTTP Client (CRITICAL)
The test suite's s.client has a cookie jar that holds the auth token from SetupTest() login. For unauthenticated tests, you MUST use a separate client without the cookie jar:
func (s *TestSuite) makeUnauthenticatedRequest(method, path string, body interface{}) (*http.Response, map[string]interface{}) {
unauthClient := &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := unauthClient.Do(req)
}
JWT Middleware Response Codes
The JWT middleware returns 302 redirect (not 401/403) for unauthenticated non-Inertia API requests. Test assertions should use NotEqual against success codes:
resp, _ := s.makeUnauthenticatedRequest("POST", "/api/entities", data)
s.NotEqual(http.StatusCreated, resp.StatusCode,
"Unauthenticated request should not create an entity")
Step 3b: Timestamp Precision in Sort Tests
TimestampsTz() creates timestamp(0) columns — second precision only. Records created within the same second have identical timestamps, making sort order indeterminate. Don't rely on millisecond sleeps for ordering:
s.createTestEntity("First")
time.Sleep(10 * time.Millisecond)
s.createTestEntity("Second")
Step 4: Add Read-Only Field Tests (if applicable)
func (s *TestSuite) TestReadOnlyFieldCannotBeSet() {
data := s.getValidData()
data["score"] = 99
resp, result := s.makeRequest("POST", "/api/entities", data)
s.Equal(http.StatusCreated, resp.StatusCode)
var entity models.Entity
facades.Orm().Query().Find(&entity, result["data"].(map[string]interface{})["id"])
s.Equal(0, entity.Score, "read-only field should NOT be settable")
}
Step 4: Verify Test Compiles
Before running tests, ensure the test file compiles:
go vet ./tests/feature/crud/...
Step 5: Run Tests (TDD Loop)
APP_ENV=testing go test -v ./tests/feature/crud -run Test<Entity>CRUDTestSuite
Fix each failure, re-run, iterate until all pass.
Test Coverage Checklist
Next Step
After ALL tests pass, proceed to frontend work:
- Run
/inertia-types to create TypeScript types and i18n translations
- OR run
/inertia-scaffold to generate all frontend components at once
Do NOT start UI work until all CRUD tests pass. Backend bugs (Bind issues, validation key mismatches, GORM mapping errors) are much easier to catch and fix via API tests than through the UI.