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