289 lines
6.8 KiB
Markdown
289 lines
6.8 KiB
Markdown
# System Test Patterns
|
|
|
|
## Purpose
|
|
|
|
System tests are black-box tests that verify the entire application works correctly from an external perspective. They test via CLI or API, simulating real user interactions.
|
|
|
|
**Location**: `tests/` directory at project root (separate from package code)
|
|
|
|
## Principles
|
|
|
|
### Black Box Testing
|
|
- Test only via public interfaces (CLI, API)
|
|
- No access to internal packages
|
|
- Simulate real user behavior
|
|
- Test critical workflows end-to-end
|
|
|
|
### Independence in Go
|
|
- Strive for pure Go tests (no Docker required)
|
|
- Use in-memory mocks from `testutils`
|
|
- Binary dependencies when needed
|
|
- Avoid docker-compose in CI
|
|
|
|
## CLI Testing Patterns
|
|
|
|
### Pattern 1: Simple Command Execution
|
|
|
|
```go
|
|
// tests/cli_test.go
|
|
package tests
|
|
|
|
import (
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCLI_Version(t *testing.T) {
|
|
// Execute CLI command
|
|
cmd := exec.Command("./myapp", "version")
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(output), "myapp version")
|
|
}
|
|
|
|
func TestCLI_Help(t *testing.T) {
|
|
cmd := exec.Command("./myapp", "--help")
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(output), "Usage:")
|
|
}
|
|
```
|
|
|
|
### Pattern 2: CLI with In-Memory Mocks
|
|
|
|
```go
|
|
// tests/cli_metrics_test.go
|
|
package tests
|
|
|
|
import (
|
|
"context"
|
|
"os/exec"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"myproject/internal/testutils"
|
|
)
|
|
|
|
func TestCLI_MetricsIngest(t *testing.T) {
|
|
// Start Victoria Metrics (Level 2 - binary)
|
|
vmServer, err := testutils.RunVictoriaMetricsServer()
|
|
require.NoError(t, err)
|
|
defer vmServer.Shutdown()
|
|
|
|
// Test CLI against real Victoria Metrics
|
|
cmd := exec.Command("./myapp", "ingest",
|
|
"--metrics-url", vmServer.WriteURL(),
|
|
"--metric-name", "cli_test_metric",
|
|
"--value", "100")
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(output), "Metric ingested successfully")
|
|
|
|
// Verify with helpers
|
|
vmServer.ForceFlush(context.Background())
|
|
results, err := testutils.QueryVictoriaMetrics(vmServer.QueryURL(), "cli_test_metric")
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 1)
|
|
}
|
|
```
|
|
|
|
### Pattern 3: CLI with File System
|
|
|
|
```go
|
|
// tests/cli_config_test.go
|
|
package tests
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCLI_ConfigFile(t *testing.T) {
|
|
// Create temp directory
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
// Write config file
|
|
configContent := `
|
|
server:
|
|
port: 8080
|
|
host: localhost
|
|
`
|
|
err := os.WriteFile(configPath, []byte(configContent), 0644)
|
|
require.NoError(t, err)
|
|
|
|
// Test CLI with config file
|
|
cmd := exec.Command("./myapp", "start", "--config", configPath, "--dry-run")
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(output), "Server would start on localhost:8080")
|
|
}
|
|
```
|
|
|
|
## API Testing Patterns
|
|
|
|
### Pattern 1: HTTP API with In-Memory Mocks
|
|
|
|
```go
|
|
// tests/api_test.go
|
|
package tests
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"os/exec"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"myproject/internal/testutils"
|
|
)
|
|
|
|
func TestAPI_UserWorkflow(t *testing.T) {
|
|
// Start in-memory NATS (Level 1)
|
|
natsServer, err := testutils.RunNATsServer()
|
|
require.NoError(t, err)
|
|
defer natsServer.Shutdown()
|
|
|
|
natsAddr := "nats://" + natsServer.Addr().String()
|
|
|
|
// Start API server
|
|
cmd := exec.Command("./myapp", "serve",
|
|
"--port", "0", // Random free port
|
|
"--nats-url", natsAddr)
|
|
|
|
// Start in background
|
|
err = cmd.Start()
|
|
require.NoError(t, err)
|
|
defer cmd.Process.Kill()
|
|
|
|
// Wait for API to be ready
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Get actual port (from logs or endpoint)
|
|
apiURL := "http://localhost:8080" // Or parse from logs
|
|
|
|
// Test API workflow
|
|
// 1. Create user
|
|
createReq := map[string]string{
|
|
"name": "Alice",
|
|
"email": "alice@example.com",
|
|
}
|
|
body, _ := json.Marshal(createReq)
|
|
|
|
resp, err := http.Post(apiURL+"/users", "application/json", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
|
|
// Parse response
|
|
var createResp map[string]string
|
|
json.NewDecoder(resp.Body).Decode(&createResp)
|
|
userID := createResp["id"]
|
|
|
|
// 2. Retrieve user
|
|
resp, err = http.Get(apiURL + "/users/" + userID)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var user map[string]string
|
|
json.NewDecoder(resp.Body).Decode(&user)
|
|
require.Equal(t, "Alice", user["name"])
|
|
}
|
|
```
|
|
|
|
## Architecture for Independence
|
|
|
|
### Dependency Injection Pattern
|
|
|
|
Design your application to accept dependency URLs:
|
|
|
|
```go
|
|
// cmd/myapp/main.go
|
|
func main() {
|
|
// Allow overriding dependencies via flags
|
|
natsURL := flag.String("nats-url", "nats://localhost:4222", "NATS server URL")
|
|
metricsURL := flag.String("metrics-url", "http://localhost:8428", "Metrics server URL")
|
|
flag.Parse()
|
|
|
|
// Use provided URLs (allows in-memory mocks in tests)
|
|
app := app.New(*natsURL, *metricsURL)
|
|
app.Run()
|
|
}
|
|
```
|
|
|
|
### Test with In-Memory Dependencies
|
|
|
|
```go
|
|
// tests/app_test.go
|
|
func TestApp_WithMocks(t *testing.T) {
|
|
// Start all mocks
|
|
natsServer, _ := testutils.RunNATsServer()
|
|
defer natsServer.Shutdown()
|
|
|
|
vmServer, _ := testutils.RunVictoriaMetricsServer()
|
|
defer vmServer.Shutdown()
|
|
|
|
// Test app with mocked dependencies (pure Go, no Docker!)
|
|
cmd := exec.Command("./myapp", "serve",
|
|
"--nats-url", "nats://"+natsServer.Addr().String(),
|
|
"--metrics-url", vmServer.WriteURL())
|
|
|
|
// ... test application
|
|
}
|
|
```
|
|
|
|
## Running System Tests
|
|
|
|
```bash
|
|
# Build application first
|
|
go build -o myapp ./cmd/myapp
|
|
|
|
# Run system tests
|
|
go test -v ./tests/...
|
|
|
|
# With coverage
|
|
go test -v -coverprofile=coverage.out ./tests/...
|
|
|
|
# Specific test
|
|
go test -v ./tests/... -run TestCLI_MetricsIngest
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### DO:
|
|
- Test via CLI/API only (black box)
|
|
- Use in-memory mocks from testutils
|
|
- Test critical end-to-end workflows
|
|
- Build binary before running tests
|
|
- Use temp directories for file operations
|
|
|
|
### DON'T:
|
|
- Don't import internal packages
|
|
- Don't test every edge case (that's unit/integration tests)
|
|
- Don't require Docker in CI
|
|
- Don't use sleep for timing (use polling/channels)
|
|
- Don't skip cleanup
|
|
|
|
## Key Takeaways
|
|
|
|
1. **Black box only** - Test via public interfaces
|
|
2. **Independent in Go** - No Docker required
|
|
3. **Use testutils mocks** - Reuse infrastructure
|
|
4. **Test critical paths** - Not every scenario
|
|
5. **Fast execution** - Should run quickly in CI
|