furt/devdocs/furt_testing_guidelines.md
michael d6d546bd95 docs(core): comprehensive development documentation and issue management system
- Complete project documentation for API gateway development
- API gateway-specific development processes and standards
- Comprehensive issue management system with script automation
- Go-specific testing guidelines for multi-service architecture

New Documentation:
- devdocs/KONZEPT.md: project philosophy, architecture, service integration patterns
- devdocs/TESTING_GUIDELINES.md: Go testing, API tests, gateway-service integration
- devdocs/development-process.md: API gateway development, multi-service coordination
- devdocs/furt-issue-management-guide.md: Furt-specific issue management workflows

Issue Management System:
- scripts/create_issue.sh: 8 preconfigured templates for API gateway development
- Furt-specific issue types: service-request, architecture, performance, security
- Script-based workflows for efficient development
- Integration with existing get_issues.sh and update_issue.sh scripts

API Gateway Development Standards:
- Service integration patterns for gateway ↔ service communication
- API-contract-first development with OpenAPI specifications
- Security-first patterns for authentication and input validation
- Multi-service testing strategies for coordinated development

This documentation enables immediate, efficient API gateway development
with clear standards, proven patterns, and automated workflows.
2025-06-03 18:45:55 +02:00

783 lines
No EOL
21 KiB
Markdown

# Furt Testing-Richtlinien
**Erstellt:** 03.06.2025
**Letzte Aktualisierung:** 03.06.2025
**Version:** 1.0
**Verantwortlich:** DAW-Team
**Dateipfad:** devdocs/TESTING_GUIDELINES.md
## Zweck dieses Dokuments
Dieses Dokument definiert verbindliche Standards und Richtlinien für das Testen von Komponenten des Furt API-Gateway-Projekts. Es soll sicherstellen, dass alle implementierten Funktionalitäten ausreichend durch Tests abgedeckt sind und die Tests konsistent und wartbar bleiben.
Es richtet sich an alle Entwickler, die Code zum Projekt beisteuern.
## 1. Grundprinzipien
### 1.1 Test-First-Entwicklung
- Tests sollten parallel zur Implementierung oder idealerweise vor der eigentlichen Implementierung geschrieben werden.
- Keine Implementierung gilt als abgeschlossen, bis entsprechende Tests vorhanden sind.
- Pull Requests ohne Tests werden in der Regel nicht akzeptiert.
### 1.2 Testabdeckung
- Angestrebte Testabdeckung für Gateway-Kern: mindestens 85%
- Angestrebte Testabdeckung für Services: mindestens 80%
- Angestrebte Testabdeckung für Shared-Libraries: mindestens 90%
- Besonders kritische Komponenten (Authentifizierung, Routing, Service-Proxy) sollten eine Abdeckung nahe 100% haben.
### 1.3 Test-Typen
Folgende Test-Typen werden im Projekt verwendet:
1. **Unit Tests**: Testen einzelner Funktionen/Methoden in Isolation
2. **Integration Tests**: Testen des Zusammenspiels von Komponenten
3. **API Tests**: Testen der API-Endpunkte des Gateways und Services
4. **Service Integration Tests**: Testen der Gateway ↔ Service-Kommunikation
5. **End-to-End Tests**: Testen der gesamten Request-Pipeline (Client → Gateway → Service)
6. **Performance Tests**: Load-Testing für Gateway und Services
## 2. Test-Struktur und Dateiorganisation
### 2.1 Dateistruktur
- Test-Dateien werden neben den zu testenden Dateien platziert und erhalten den Suffix `_test.go`
- Beispiel: `gateway.go``gateway_test.go`
- Integration-Tests werden in `tests/integration/` platziert
- End-to-End-Tests werden in `tests/e2e/` platziert
### 2.2 Namenkonventionen
- Testfunktionen folgen dem Format `Test<Komponente><Funktionsname><Szenario>`
- Beispiel: `TestGatewayRoutingWithValidAPIKey`, `TestServiceProxyWhenServiceUnavailable`
- Benchmark-Tests: `Benchmark<Funktionsname>`
- Example-Tests: `Example<Funktionsname>`
### 2.3 Testpakete
- Tests sollten im selben Paket wie der zu testende Code sein (kein separates `_test`-Paket)
- Dies ermöglicht das Testen von Funktionen, die nicht exportiert werden
- Ausnahme: Integration-Tests können separate Pakete verwenden
## 3. Unit Tests
### 3.1 Grundstruktur
Jeder Unit Test sollte folgende Struktur haben:
```go
func TestFunctionName(t *testing.T) {
// Arrange: Vorbereitung der Testdaten und Abhängigkeiten
input := setupTestInput()
mockService := &MockService{}
expected := expectedResult{}
// Act: Ausführen der zu testenden Funktion
actual, err := FunctionName(input, mockService)
// Assert: Überprüfung des Ergebnisses
if err != nil {
t.Fatalf("Expected no error, but got: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Expected %+v, but got %+v", expected, actual)
}
}
```
### 3.2 Table-Driven Tests
Für komplexere Funktionen sollten Table-Driven Tests verwendet werden:
```go
func TestGatewayRouting(t *testing.T) {
testCases := []struct {
name string
requestPath string
expectedService string
expectedPath string
wantErr bool
}{
{
name: "formular2mail service routing",
requestPath: "/v1/mail/send",
expectedService: "formular2mail",
expectedPath: "/send",
wantErr: false,
},
{
name: "sagjan service routing",
requestPath: "/v1/comments/list",
expectedService: "sagjan",
expectedPath: "/list",
wantErr: false,
},
{
name: "unknown service",
requestPath: "/v1/unknown/test",
expectedService: "",
expectedPath: "",
wantErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gateway := setupTestGateway()
service, path, err := gateway.ResolveRoute(tc.requestPath)
if tc.wantErr && err == nil {
t.Error("Expected error, but got nil")
}
if !tc.wantErr && err != nil {
t.Fatalf("Expected no error, but got: %v", err)
}
if service != tc.expectedService {
t.Errorf("Expected service %s, got %s", tc.expectedService, service)
}
if path != tc.expectedPath {
t.Errorf("Expected path %s, got %s", tc.expectedPath, path)
}
})
}
}
```
### 3.3 Mocking und Test-Doubles
- Für HTTP-Clients verwende `httptest.NewServer`
- Für Services erstelle Interface-basierte Mocks
- Verwende Dependency Injection für bessere Testbarkeit
```go
// Service Interface für Testbarkeit
type MailService interface {
SendMail(request MailRequest) error
}
// Mock Implementation
type MockMailService struct {
SendMailFunc func(MailRequest) error
CallCount int
}
func (m *MockMailService) SendMail(request MailRequest) error {
m.CallCount++
if m.SendMailFunc != nil {
return m.SendMailFunc(request)
}
return nil
}
// Test mit Mock
func TestFormular2MailHandler(t *testing.T) {
mockService := &MockMailService{
SendMailFunc: func(req MailRequest) error {
if req.Email == "invalid" {
return errors.New("invalid email")
}
return nil
},
}
handler := NewFormular2MailHandler(mockService)
// Test ausführen...
}
```
## 4. Integration Tests
### 4.1 Gateway-Service Integration Tests
Diese Tests prüfen die Kommunikation zwischen Gateway und Services:
```go
// tests/integration/gateway_service_test.go
func TestGatewayServiceIntegration(t *testing.T) {
// Setup Test-Services
mailService := startTestMailService(t)
defer mailService.Close()
commentsService := startTestCommentsService(t)
defer commentsService.Close()
// Setup Gateway mit Test-Konfiguration
gateway := setupTestGateway(t, GatewayConfig{
Services: map[string]ServiceConfig{
"formular2mail": {
Enabled: true,
PathPrefix: "/v1/mail",
Upstream: mailService.URL,
},
"sagjan": {
Enabled: true,
PathPrefix: "/v1/comments",
Upstream: commentsService.URL,
},
},
})
defer gateway.Close()
// Test Gateway → Service Routing
t.Run("mail service integration", func(t *testing.T) {
resp := makeTestRequest(t, gateway.URL+"/v1/mail/send", "POST", mailRequestBody)
assertStatusCode(t, resp, http.StatusOK)
})
t.Run("comments service integration", func(t *testing.T) {
resp := makeTestRequest(t, gateway.URL+"/v1/comments", "GET", nil)
assertStatusCode(t, resp, http.StatusOK)
})
}
```
### 4.2 Database Integration Tests (für Services)
Für Services mit Datenbank-Zugriff:
```go
func TestSagjanServiceDatabaseIntegration(t *testing.T) {
// Setup Test-Database (SQLite in-memory)
db := setupTestDB(t)
defer db.Close()
service := NewSagjanService(db)
// Test Comment Creation
comment := &Comment{
PageURL: "https://example.com/test",
Author: "Test User",
Content: "Test Comment",
}
err := service.CreateComment(context.Background(), comment)
if err != nil {
t.Fatalf("Failed to create comment: %v", err)
}
// Test Comment Retrieval
comments, err := service.GetComments(context.Background(), "https://example.com/test")
if err != nil {
t.Fatalf("Failed to get comments: %v", err)
}
if len(comments) != 1 {
t.Errorf("Expected 1 comment, got %d", len(comments))
}
}
func setupTestDB(t *testing.T) *sql.DB {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open test database: %v", err)
}
// Run migrations
if err := runMigrations(db); err != nil {
t.Fatalf("Failed to run migrations: %v", err)
}
return db
}
```
## 5. API Tests
### 5.1 Gateway API Tests
Tests für die Gateway-API-Endpunkte:
```go
func TestGatewayAPIEndpoints(t *testing.T) {
gateway := setupTestGateway(t)
defer gateway.Close()
testCases := []struct {
name string
method string
path string
headers map[string]string
body string
expectedStatus int
expectedBody string
}{
{
name: "health check",
method: "GET",
path: "/health",
expectedStatus: http.StatusOK,
expectedBody: `{"status":"healthy"}`,
},
{
name: "unauthorized request",
method: "POST",
path: "/v1/mail/send",
expectedStatus: http.StatusUnauthorized,
},
{
name: "authorized mail request",
method: "POST",
path: "/v1/mail/send",
headers: map[string]string{
"X-API-Key": "test-api-key",
"Content-Type": "application/json",
},
body: `{"name":"Test","email":"test@example.com","message":"Test"}`,
expectedStatus: http.StatusOK,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := createTestRequest(t, tc.method, gateway.URL+tc.path, tc.body)
for key, value := range tc.headers {
req.Header.Set(key, value)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != tc.expectedStatus {
t.Errorf("Expected status %d, got %d", tc.expectedStatus, resp.StatusCode)
}
if tc.expectedBody != "" {
body, _ := io.ReadAll(resp.Body)
if string(body) != tc.expectedBody {
t.Errorf("Expected body %s, got %s", tc.expectedBody, string(body))
}
}
})
}
}
```
### 5.2 Service API Tests
Tests für individuelle Service-APIs:
```go
func TestFormular2MailAPI(t *testing.T) {
service := startTestFormular2MailService(t)
defer service.Close()
t.Run("valid mail request", func(t *testing.T) {
reqBody := `{
"name": "John Doe",
"email": "john@example.com",
"message": "Test message"
}`
resp := makeTestRequest(t, service.URL+"/send", "POST", reqBody)
assertStatusCode(t, resp, http.StatusOK)
var response MailResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
if !response.Success {
t.Error("Expected success=true in response")
}
})
t.Run("invalid mail request", func(t *testing.T) {
reqBody := `{"name": "", "email": "invalid", "message": ""}`
resp := makeTestRequest(t, service.URL+"/send", "POST", reqBody)
assertStatusCode(t, resp, http.StatusBadRequest)
})
}
```
## 6. Performance Tests
### 6.1 Gateway Performance Tests
```go
func TestGatewayPerformance(t *testing.T) {
if testing.Short() {
t.Skip("Skipping performance test in short mode")
}
gateway := setupTestGateway(t)
defer gateway.Close()
// Load test
concurrency := 10
requests := 1000
var wg sync.WaitGroup
errors := make(chan error, requests)
start := time.Now()
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < requests/concurrency; j++ {
resp, err := http.Get(gateway.URL + "/health")
if err != nil {
errors <- err
return
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
errors <- fmt.Errorf("unexpected status: %d", resp.StatusCode)
return
}
}
}()
}
wg.Wait()
close(errors)
duration := time.Since(start)
// Check for errors
for err := range errors {
t.Errorf("Request error: %v", err)
}
// Performance assertions
requestsPerSecond := float64(requests) / duration.Seconds()
if requestsPerSecond < 500 { // Minimum 500 RPS
t.Errorf("Performance too low: %.2f RPS", requestsPerSecond)
}
t.Logf("Performance: %.2f RPS over %v", requestsPerSecond, duration)
}
func BenchmarkGatewayRouting(b *testing.B) {
gateway := setupBenchmarkGateway(b)
req := httptest.NewRequest("GET", "/v1/mail/send", nil)
req.Header.Set("X-API-Key", "test-key")
b.ResetTimer()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
gateway.ServeHTTP(w, req)
}
}
```
## 7. Test-Daten und Test-Utilities
### 7.1 Test-Daten-Management
```go
// internal/testutil/fixtures.go
package testutil
func CreateTestMailRequest() MailRequest {
return MailRequest{
Name: "Test User",
Email: "test@example.com",
Subject: "Test Subject",
Message: "Test Message",
}
}
func CreateTestComment() *Comment {
return &Comment{
ID: uuid.New().String(),
PageURL: "https://example.com/test",
Author: "Test Author",
Email: "test@example.com",
Content: "Test Comment Content",
Status: StatusPending,
}
}
func CreateTestGatewayConfig() GatewayConfig {
return GatewayConfig{
Gateway: GatewaySettings{
Port: "8080",
LogLevel: "info",
},
Security: SecurityConfig{
APIKeys: []APIKey{
{
Key: "test-api-key",
Name: "Test Key",
Permissions: []string{"mail:send"},
AllowedIPs: []string{"127.0.0.1"},
},
},
},
Services: map[string]ServiceConfig{
"formular2mail": {
Enabled: true,
PathPrefix: "/v1/mail",
Upstream: "http://127.0.0.1:8081",
HealthCheck: "/health",
Timeout: 30 * time.Second,
},
},
}
}
```
### 7.2 Test-Helper-Funktionen
```go
// internal/testutil/helpers.go
package testutil
func AssertStatusCode(t *testing.T, resp *http.Response, expected int) {
t.Helper()
if resp.StatusCode != expected {
t.Errorf("Expected status code %d, got %d", expected, resp.StatusCode)
}
}
func AssertResponseBody(t *testing.T, resp *http.Response, expected string) {
t.Helper()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if string(body) != expected {
t.Errorf("Expected body %q, got %q", expected, string(body))
}
}
func MakeTestRequest(t *testing.T, url, method, body string) *http.Response {
t.Helper()
var reqBody io.Reader
if body != "" {
reqBody = strings.NewReader(body)
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
if body != "" {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
return resp
}
```
## 8. Test-Umgebung und CI
### 8.1 Lokale Tests
- Alle Tests sollten mit `go test ./...` ausführbar sein
- Keine Tests sollten externe Ressourcen benötigen (wie echte E-Mail-Server)
- Performance-Tests mit `-short` Flag überspringen
### 8.2 Test-Tags
```go
// +build integration
package tests
// Integration tests that require external resources
```
**Ausführung:**
```bash
# Nur Unit Tests
go test ./...
# Mit Integration Tests
go test -tags=integration ./...
# Mit Performance Tests
go test -timeout=30m ./...
# Kurze Tests für CI
go test -short ./...
```
### 8.3 Coverage-Berichte
```bash
# Coverage generieren
go test -coverprofile=coverage.out ./...
# HTML-Report
go tool cover -html=coverage.out -o coverage.html
# Coverage-Threshold prüfen
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//'
```
## 9. Spezifische Testfälle für Furt
### 9.1 Gateway-Routing Tests
```go
func TestGatewayServiceRouting(t *testing.T) {
testCases := []struct {
name string
requestPath string
method string
expectedService string
expectedUpstream string
wantErr bool
}{
{
name: "formular2mail routing",
requestPath: "/v1/mail/send",
method: "POST",
expectedService: "formular2mail",
expectedUpstream: "http://127.0.0.1:8081",
},
{
name: "sagjan comments routing",
requestPath: "/v1/comments",
method: "GET",
expectedService: "sagjan",
expectedUpstream: "http://127.0.0.1:8082",
},
{
name: "unknown service",
requestPath: "/v1/unknown",
method: "GET",
wantErr: true,
},
}
// Implementation...
}
```
### 9.2 Authentication Tests
```go
func TestGatewayAuthentication(t *testing.T) {
testCases := []struct {
name string
apiKey string
clientIP string
requestPath string
expectedStatus int
}{
{
name: "valid API key and IP",
apiKey: "hugo-frontend-key",
clientIP: "127.0.0.1",
requestPath: "/v1/mail/send",
expectedStatus: http.StatusOK,
},
{
name: "invalid API key",
apiKey: "invalid-key",
clientIP: "127.0.0.1",
requestPath: "/v1/mail/send",
expectedStatus: http.StatusUnauthorized,
},
{
name: "blocked IP",
apiKey: "hugo-frontend-key",
clientIP: "192.168.1.100",
requestPath: "/v1/mail/send",
expectedStatus: http.StatusForbidden,
},
}
// Implementation...
}
```
### 9.3 Service Health Check Tests
```go
func TestServiceHealthChecks(t *testing.T) {
// Test Gateway health aggregation
t.Run("all services healthy", func(t *testing.T) {
// Setup healthy services
// Test /health returns 200 with all services status
})
t.Run("one service unhealthy", func(t *testing.T) {
// Setup one failing service
// Test /health returns appropriate status
})
}
```
## 10. Test-Automation und CI-Integration
### 10.1 GitHub Actions / Gitea Actions
```yaml
# .gitea/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.21
- name: Run Unit Tests
run: go test -short -race -coverprofile=coverage.out ./...
- name: Run Integration Tests
run: go test -tags=integration ./tests/integration/
- name: Check Coverage
run: |
coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
if (( $(echo "$coverage < 80" | bc -l) )); then
echo "Coverage $coverage% is below 80%"
exit 1
fi
```
## 11. Best Practices Zusammenfassung
### 11.1 Do's
-**Testbare Architektur:** Dependency Injection verwenden
-**Isolierte Tests:** Keine Abhängigkeiten zwischen Tests
-**Realistische Test-Daten:** Aber anonymisiert und minimal
-**Performance-bewusst:** Benchmarks für kritische Pfade
-**Dokumentierte Test-Fälle:** Klare Beschreibungen der Test-Szenarien
### 11.2 Don'ts
-**Externe Ressourcen:** Keine echten E-Mail-Server, externe APIs
-**Feste Zeitstempel:** `time.Now()` mocken in Tests
-**Globaler State:** Tests sollten unabhängig sein
-**Überflüssige Tests:** Triviale Getter/Setter nicht testen
-**Fragile Tests:** Tests sollen bei kleinen Änderungen nicht brechen
---
Diese Richtlinien sollen als Leitfaden dienen und können im Laufe des Projekts angepasst und erweitert werden. Bei Unklarheiten oder Fragen zu diesen Richtlinien kann das Entwicklungsteam kontaktiert werden.