783 lines
21 KiB
Markdown
783 lines
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.
|