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

21 KiB

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.gogateway_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:

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:

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
// 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:

// 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:

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:

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:

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

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

// 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

// 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

// +build integration

package tests

// Integration tests that require external resources

Ausführung:

# 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

# 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

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

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

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

# .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.