testing.md
Testing
Patterns for testing ReadySite applications.
Running Tests
go test ./... # All tests
go test -v ./pkg/database/... # Single package
go test -run TestCollection ./... # Single test by name
go test -v -run TestCollection/Insert ./pkg/database # Subtest
Database Testing
Use in-memory databases for fast, isolated tests:
import (
"github.com/readysite/readysite/pkg/database"
"github.com/readysite/readysite/pkg/database/engines"
)
type TestEntity struct {
database.Model
Name string
Status string
}
func TestInsert(t *testing.T) {
db := engines.NewMemory()
entities := database.Manage(db, new(TestEntity))
entity := &TestEntity{Name: "test", Status: "active"}
if _, err := entities.Insert(entity); err != nil {
t.Fatalf("insert failed: %v", err)
}
if entity.ID == "" {
t.Error("expected ID to be generated")
}
found, err := entities.Get(entity.ID)
if err != nil {
t.Fatalf("get failed: %v", err)
}
if found.Name != "test" {
t.Errorf("expected 'test', got %q", found.Name)
}
}
Indexes in Tests
Add indexes just like production code:
db := engines.NewMemory()
users := database.Manage(db, new(User),
database.WithUniqueIndex[User]("Email"),
database.WithIndex[User]("Role"),
)
Controller Testing
Test Setup Pattern
Use sync.Once for one-time test database initialization:
var (
setupOnce sync.Once
testApp *application.App
testAdmin *models.User
)
func setupTestDB() {
setupOnce.Do(func() {
// Replace with in-memory database
models.DB = engines.NewMemory()
// Reinitialize all collections
models.Users = database.Manage(models.DB, new(models.User),
database.WithUniqueIndex[models.User]("Email"),
)
models.Posts = database.Manage(models.DB, new(models.Post))
// Create test user
testAdmin = &models.User{
Email: "admin@test.com",
Name: "Test Admin",
Role: "admin",
}
testAdmin.ID = "test-admin"
models.Users.Insert(testAdmin)
// Create test app
testApp = application.New(
application.WithViews(testViews),
application.WithController(Blog()),
)
})
}
func init() {
setupTestDB()
}
HTTP Request Testing
func TestCreate(t *testing.T) {
formData := url.Values{
"title": {"My Post"},
"slug": {"my-post"},
"content": {"Hello world"},
}
req := httptest.NewRequest("POST", "/blog",
strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
authenticateRequest(req)
w := httptest.NewRecorder()
c := &BlogController{}
c.BaseController.Setup(testApp)
handler := c.Handle(req)
handler.(*BlogController).Create(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("expected 303, got %d", w.Code)
}
}
Path Values
Set path values for routes with parameters:
req := httptest.NewRequest("GET", "/blog/my-post", nil)
req.SetPathValue("slug", "my-post")
Authenticated Requests
Create a JWT cookie for authenticated tests:
func authenticateRequest(req *http.Request) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": testAdmin.ID,
"exp": time.Now().Add(24 * time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte("dev-only-not-for-production"))
req.AddCookie(&http.Cookie{
Name: "session",
Value: tokenString,
})
}
HTMX Request Testing
// Test HTMX redirect behavior
req := httptest.NewRequest("POST", "/blog", body)
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
// HTMX redirects return 200 + HX-Location header
if w.Code != http.StatusOK {
t.Errorf("HTMX redirect should be 200, got %d", w.Code)
}
if loc := w.Header().Get("HX-Location"); loc != "/blog/my-post" {
t.Errorf("expected HX-Location, got %q", loc)
}
File Upload Testing
func TestUpload(t *testing.T) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile("file", "test.txt")
part.Write([]byte("Hello, World!"))
writer.Close()
req := httptest.NewRequest("POST", "/files", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
// ... invoke controller method
}
Mock Assistant
Use the mock provider for AI tests without API calls:
Basic Response
import "github.com/readysite/readysite/pkg/assistant/providers/mock"
ai := mock.New(mock.Config{
Response: "Hello, World!",
})
resp, err := ai.Chat(ctx, assistant.ChatRequest{
Messages: []assistant.Message{
assistant.NewUserMessage("Hi"),
},
})
// resp.Content == "Hello, World!"
// resp.FinishReason == "stop"
With Tool Calls
ai := mock.New(mock.Config{
Response: "Let me create that page.",
ToolCalls: []assistant.ToolCall{
{ID: "call_1", Name: "create_page", Arguments: `{"title":"Test"}`},
},
})
resp, _ := ai.Chat(ctx, assistant.ChatRequest{
Messages: []assistant.Message{
assistant.NewUserMessage("Create a page"),
},
Tools: []assistant.Tool{
assistant.NewTool("create_page", "Create a page").
String("title", "Page title", true).
Build(),
},
})
// resp.FinishReason == "tool_calls"
// len(resp.ToolCalls) == 1
Streaming
ai := mock.New(mock.Config{
Response: "Hello World",
StreamDelay: 10 * time.Millisecond,
})
stream, _ := ai.Stream(ctx, req)
defer stream.Close()
resp, _ := stream.Collect()
// resp.Content == "Hello World"
Error Simulation
ai := mock.New(mock.Config{
Error: fmt.Errorf("API unavailable"),
})
_, err := ai.Chat(ctx, req)
// err != nil
Platform Mocking
Use the mock platform provider for infrastructure tests:
import "github.com/readysite/readysite/pkg/platform/providers/mock"
p := mock.New()
server, _ := p.CreateServer(platform.ServerOptions{
Name: "test-server",
Size: platform.Small,
Region: platform.NYC,
})
// server.Status == "active"
volume, _ := p.CreateVolume("test-vol", 50, platform.NYC)
p.AttachVolume(volume.ID, server.ID)
Table-Driven Tests
Follow Go conventions with table-driven tests:
func TestSlugValidation(t *testing.T) {
tests := []struct {
name string
slug string
wantErr bool
}{
{"valid", "my-page", false},
{"valid single char", "a", false},
{"empty", "", true},
{"uppercase", "My-Page", true},
{"starts with hyphen", "-page", true},
{"reserved", "admin", true},
{"too long", strings.Repeat("a", 51), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := content.ValidateSlug(tt.slug)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateSlug(%q) error = %v, wantErr %v", tt.slug, err, tt.wantErr)
}
})
}
}
Test Data
Use embedded test views for template tests:
//go:embed testdata/views
var testViewsRaw embed.FS
var testViews, _ = fs.Sub(testViewsRaw, "testdata")
Create minimal test templates in testdata/views/:
testdata/views/
├── layouts/
│ └── base.html
├── partials/
│ └── card.html
└── pages/
└── test.html
Test File Organization
myapp/
├── controllers/
│ ├── blog.go
│ ├── blog_test.go # Tests for blog controller
│ └── testhelper_test.go # Shared test setup
├── models/
│ ├── post.go
│ └── post_test.go # Model tests
└── internal/
├── validator.go
└── validator_test.go # Internal package tests