# 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` - Beispiel: `TestGatewayRoutingWithValidAPIKey`, `TestServiceProxyWhenServiceUnavailable` - Benchmark-Tests: `Benchmark` - Example-Tests: `Example` ### 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.