🎯 refactor: implement comprehensive BDD test suite with modular architecture
Some checks failed
CI/CD Pipeline / Build Docker Cache (push) Successful in 9s
CI/CD Pipeline / CI Pipeline (push) Failing after 3m5s

 feat: add feature-based test organization per ADR 0024
🐛 fix: resolve compilation errors in suite_feature.go
📝 docs: add comprehensive BDD framework documentation
♻️ refactor: split monolithic tests into modular features
🧪 test: implement synchronization helpers and context management
 perf: add parallel test execution capability
🔧 chore: add feature-specific test scripts and validation
📚 docs: move BDD_TAGS.md to features/ for better organization

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-04-10 00:00:52 +02:00
parent de22839eb7
commit de2e03519e
22 changed files with 1257 additions and 120 deletions

View File

@@ -1,96 +1,327 @@
# BDD Testing with Godog
# BDD Testing Framework
This package implements Behavior-Driven Development (BDD) testing using the Godog framework.
This directory contains the Behavior-Driven Development (BDD) testing framework for the dance-lessons-coach project, implementing the architecture described in ADR 0024.
## Important Requirements for Step Definitions
## 🗺️ Architecture Overview
### Step Pattern Matching
The BDD framework follows a modular, isolated test suite architecture with these key components:
Godog has **very specific requirements** for step pattern matching. To avoid "undefined" warnings:
### 📁 Directory Structure
1. **Use the exact regex pattern** that Godog suggests in its error messages
2. **Use the exact parameter names** that Godog suggests (`arg1, arg2`, etc.)
3. **Match the feature file syntax exactly** including quotes and JSON formatting
### Example
**Feature file step:**
```gherkin
Then the response should be "{\"message\":\"Hello world!\"}"
```
pkg/bdd/
├── README.md # This file
├── context/ # Feature-specific test contexts
│ ├── auth_context.go # Authentication test context
│ └── config_context.go # Configuration test context
├── helpers/ # Test synchronization helpers
│ └── synchronization.go # Wait functions and utilities
├── parallel/ # Parallel test execution
│ ├── port_manager.go # Port allocation system
│ └── resource_monitor.go # Resource tracking
├── steps/ # Step definitions
│ ├── auth_steps.go # Authentication steps
│ ├── config_steps.go # Configuration steps
│ ├── greet_steps.go # Greeting steps
│ ├── health_steps.go # Health check steps
│ ├── jwt_retention_steps.go # JWT retention steps
│ └── steps.go # Main step registration
├── suite.go # Test suite initialization
├── suite_feature.go # Feature-specific suite support
└── testserver/ # Test server implementation
├── client.go # HTTP test client
└── server.go # Test server with config
```
**Correct step definition:**
## 🎯 Core Components
### 1. Test Server
**Location:** `pkg/bdd/testserver/`
The test server provides a real HTTP server instance for black-box testing:
- **Hybrid Testing**: Runs in-process (not external process)
- **Configuration**: Loads feature-specific configs from `features/*/*-test-config.yaml`
- **Database**: Manages PostgreSQL connections with proper isolation
- **Port Management**: Uses feature-specific ports (9192-9196)
**Key Functions:**
- `NewServer()` - Creates test server instance
- `Start()` - Starts server with feature-specific configuration
- `initDBConnection()` - Initializes database connection
- `createTestConfig()` - Loads feature-specific configuration
### 2. Step Definitions
**Location:** `pkg/bdd/steps/`
Step definitions implement the Gherkin scenarios using Godog:
- **Domain-Specific**: Organized by feature area (auth, config, greet, etc.)
- **Reusable**: Common patterns in `common_steps.go`
- **Exact Matching**: Uses Godog's exact regex patterns
**Example:**
```go
ctx.Step(`^the response should be "{\"([^"]*)\":\"([^"]*)\"}"$`, func(arg1, arg2 string) error {
// Implementation here
return nil
})
// greet_steps.go
func (gs *GreetSteps) iRequestAGreetingFor(name string) error {
return gs.client.Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
}
```
**Incorrect patterns that cause "undefined" warnings:**
### 3. Synchronization Helpers
**Location:** `pkg/bdd/helpers/`
Helpers provide robust waiting mechanisms for async operations:
- **Timeout Support**: All functions include timeout parameters
- **Polling**: Uses context-based polling with configurable intervals
- **Common Patterns**: Covers server readiness, config reload, API availability
**Available Helpers:**
- `waitForServerReady()` - Waits for server to be ready
- `waitForConfigReload()` - Detects configuration changes
- `waitForCondition()` - Generic condition waiting
- `waitForV2APIEnabled()` - Checks v2 API availability
### 4. Parallel Testing
**Location:** `pkg/bdd/parallel/`
Parallel execution infrastructure for CI/CD optimization:
- **Port Management**: `PortManager` allocates unique ports
- **Resource Monitoring**: Tracks memory, goroutines, CPU usage
- **Controlled Parallelism**: `ParallelTestRunner` limits concurrency
**Key Features:**
- Thread-safe port allocation
- Resource limit enforcement
- Timeout detection
- Comprehensive monitoring
### 5. Feature Contexts
**Location:** `pkg/bdd/context/`
Feature-specific test contexts for better organization:
- **AuthContext**: User management and authentication
- **ConfigContext**: Configuration file handling
- **Extensible**: Easy to add new feature contexts
## 🚀 Test Execution
### Running All Tests
```bash
# Default: Run all features sequentially
go test ./features/...
# With environment variables
DLC_DATABASE_HOST=localhost DLC_DATABASE_PORT=5432 \
DLC_DATABASE_USER=postgres DLC_DATABASE_PASSWORD=postgres \
DLC_DATABASE_NAME=dance_lessons_coach_bdd_test \
DLC_DATABASE_SSL_MODE=disable \
go test ./features/...
```
### Feature-Specific Testing
```bash
# Test specific feature
./scripts/test-feature.sh greet
# Test with specific tags
./scripts/test-by-tag.sh @smoke greet
```
### Parallel Testing
```bash
# Run all features in parallel
./scripts/test-all-features-parallel.sh
# Run specific features in parallel
# (Requires PostgreSQL container running)
```
### Tag-Based Testing
```bash
# List available tags
./scripts/run-bdd-tests.sh list-tags
# Run smoke tests
./scripts/run-bdd-tests.sh run @smoke
# Run critical tests for auth
./scripts/run-bdd-tests.sh run @critical @auth
```
## 📋 Test Organization
### Feature Structure
Each feature follows this structure:
```
features/{feature}/
├── {feature}.feature # Gherkin scenarios
├── {feature}-test-config.yaml # Feature-specific config
└── {feature}_test.go # Go test runner
```
### Configuration Files
Feature-specific YAML files define test environment:
```yaml
# features/greet/greet-test-config.yaml
server:
host: "127.0.0.1"
port: 9194
database:
host: "localhost"
port: 5432
name: "dance_lessons_coach_greet_test"
api:
v2_enabled: true
```
### Tagging System
Comprehensive tagging for selective test execution:
- **Feature Tags**: `@auth`, `@config`, `@greet`, `@health`, `@jwt`
- **Priority Tags**: `@smoke`, `@critical`, `@basic`, `@advanced`
- **Component Tags**: `@api`, `@v2`, `@database`, `@security`
See `features/BDD_TAGS.md` for complete documentation.
## 🔧 Database Management
### Database Creation
The framework handles database creation automatically:
1. **PostgreSQL Container**: Uses Docker (`dance-lessons-coach-postgres`)
2. **Feature Databases**: Creates `dance_lessons_coach_{feature}_test` per feature
3. **Cleanup**: Automatically drops databases after tests
**Database Creation Flow:**
1. Check if database exists
2. Create if missing (`createdb` command)
3. Run tests with isolated database
4. Cleanup (`dropdb` command)
### Configuration
Database settings come from:
- Environment variables (`DLC_DATABASE_*`)
- Feature-specific config files
- Default values for development
## 🧪 Best Practices
### Step Definition Patterns
```go
// Wrong: Different regex pattern
ctx.Step(`^the response should be "{\"message\":\"([^"]*)\"}"$`, func(message string) error {
// ...
})
// ✅ DO: Use Godog's exact regex patterns
ctx.Step(`^I request a greeting for "([^"]*)"$`, sc.iRequestAGreetingFor)
// Wrong: Different parameter names
ctx.Step(`^the response should be "{\"([^"]*)\":\"([^"]*)\"}"$`, func(key, value string) error {
// ...
})
// ❌ DON'T: Use different patterns
ctx.Step(`^I request greeting "(.*)"$`, sc.iRequestAGreetingFor)
```
## Current Implementation
### Test Isolation
### Step Definition Strategy
- Each feature has unique port and database
- No shared state between features
- Cleanup after each test run
- Feature-specific configuration
1. **First eliminate "undefined" warnings** by using Godog's exact suggested patterns
2. **Return `godog.ErrPending`** initially to confirm pattern matching works
3. **Then implement actual validation** logic
### Synchronization
### Files
```go
// ✅ DO: Use helpers for async operations
helpers.waitForServerReady(client, 30*time.Second)
- `suite.go`: Test suite initialization and server management
- `testserver/`: Test server and client implementation
- `steps/`: Step definitions for each feature
// ❌ DON'T: Use fixed sleep times
time.Sleep(5 * time.Second)
```
## Debugging "Undefined" Steps
### Context Management
If you see "undefined" warnings:
```go
// ✅ DO: Use feature-specific contexts
switch featureName {
case "auth":
authCtx = context.NewAuthContext(client)
context.InitializeAuthContext(ctx, client)
}
```
1. Run the tests to see Godog's suggested pattern:
```bash
go test ./features/... -v
```
## 📈 Performance Optimization
2. Copy the **exact regex pattern** from the error message
3. Copy the **exact parameter names** (`arg1, arg2`, etc.)
4. Update your step definition to match exactly
### Parallel Execution
## Common Mistakes
- Use `scripts/test-all-features-parallel.sh` for CI/CD
- Limit parallelism based on system resources
- Monitor resource usage with `ResourceMonitor`
The "undefined" warnings are **not a Godog bug** - they occur when step definitions don't match Godog's expected patterns exactly:
### Selective Testing
- Using different regex patterns than what Godog suggests
- Using descriptive parameter names instead of `arg1, arg2`
- Not escaping quotes properly in JSON patterns
- Trying to be "clever" with regex optimization
- Run only relevant tests with tag filtering
- Use `@smoke` for quick validation
- Use `@critical` for essential path testing
**Solution**: Always use the exact pattern and parameter names that Godog suggests in its error messages.
### Resource Management
## Best Practices
- Set appropriate timeouts
- Limit maximum goroutines
- Monitor memory usage
- Cleanup resources promptly
1. **Follow Godog's suggestions exactly** - Copy-paste the pattern and parameter names
2. **Test pattern matching first** - Use `godog.ErrPending` to verify patterns work
3. **Then implement logic** - Replace `godog.ErrPending` with actual validation
4. **Don't over-optimize regex** - Use the patterns Godog provides, even if they seem verbose
5. **One pattern per step type** - Use generic patterns to cover similar steps
## 🔧 Troubleshooting
## Why This Matters
### Common Issues
Godog's step matching is **very specific by design**:
- It needs to reliably match feature file steps to code
- It provides exact patterns to ensure consistency
- Following its suggestions guarantees your steps will be recognized
| Issue | Cause | Solution |
|-------|-------|----------|
| Undefined steps | Step pattern mismatch | Use Godog's exact suggested patterns |
| Port conflicts | Multiple servers | Check port allocation in config files |
| Database connection | PostgreSQL not running | Start with `docker compose up -d postgres` |
| Test isolation | Shared state | Verify unique ports/databases per feature |
**Remember**: The "undefined" warnings are Godog telling you exactly how to fix your step definitions!
### Debugging
```bash
# Verbose output
go test ./features/... -v
# Check specific feature
cd features/greet && go test -v .
# List available tags
./scripts/run-bdd-tests.sh list-tags
```
## 📚 Documentation
- **ADR 0024**: BDD Test Organization and Isolation Strategy
- **BDD_TAGS.md**: Complete tag reference
- **Godog Documentation**: https://github.com/cucumber/godog
## 🎯 Future Enhancements
- **Test Impact Analysis**: Track which tests are affected by code changes
- **Flaky Test Detection**: Automatically identify and quarantine flaky tests
- **Performance Benchmarking**: Monitor test execution times
- **AI-Assisted Testing**: Automated test generation and optimization
This BDD framework provides a robust foundation for behavior-driven development in the dance-lessons-coach project, ensuring test reliability, maintainability, and scalability.

View File

@@ -2,6 +2,7 @@ package context
import (
"dance-lessons-coach/pkg/bdd/testserver"
"github.com/cucumber/godog"
)

View File

@@ -2,6 +2,7 @@ package context
import (
"dance-lessons-coach/pkg/bdd/testserver"
"github.com/cucumber/godog"
)

View File

@@ -6,6 +6,7 @@ import (
"time"
"dance-lessons-coach/pkg/bdd/testserver"
"github.com/rs/zerolog/log"
)

View File

@@ -18,9 +18,19 @@ type ConfigSteps struct {
}
func NewConfigSteps(client *testserver.Client) *ConfigSteps {
// Get feature-specific config path
feature := os.Getenv("FEATURE")
var configFilePath string
if feature != "" {
configFilePath = fmt.Sprintf("features/%s/%s-test-config.yaml", feature, feature)
} else {
configFilePath = "test-config.yaml"
}
return &ConfigSteps{
client: client,
configFilePath: "test-config.yaml",
configFilePath: configFilePath,
}
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
@@ -27,8 +28,37 @@ type Server struct {
}
func NewServer() *Server {
// Get feature-specific port from configuration
feature := os.Getenv("FEATURE")
port := 9191 // Default port
if feature != "" {
// Try to read port from feature-specific config
configPath := fmt.Sprintf("features/%s/%s-test-config.yaml", feature, feature)
if _, statErr := os.Stat(configPath); statErr == nil {
// Read config file to get port
content, err := os.ReadFile(configPath)
if err == nil {
// Simple YAML parsing to extract port
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.Contains(line, "port:") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
portStr := strings.TrimSpace(parts[1])
if p, err := strconv.Atoi(portStr); err == nil {
port = p
break
}
}
}
}
}
}
}
return &Server{
port: 9191,
port: port,
}
}
@@ -71,7 +101,16 @@ func (s *Server) Start() error {
// monitorConfigFile monitors the test config file for changes and reloads configuration
func (s *Server) monitorConfigFile() {
testConfigPath := "test-config.yaml"
// Get feature-specific config path
feature := os.Getenv("FEATURE")
var testConfigPath string
if feature != "" {
testConfigPath = fmt.Sprintf("features/%s/%s-test-config.yaml", feature, feature)
} else {
testConfigPath = "test-config.yaml"
}
lastModTime := time.Time{}
fileExists := false
@@ -151,7 +190,39 @@ func (s *Server) ReloadConfig() error {
// initDBConnection initializes a direct database connection for cleanup operations
func (s *Server) initDBConnection() error {
cfg := createTestConfig(s.port)
// Get feature-specific configuration
feature := os.Getenv("FEATURE")
var cfg *config.Config
if feature != "" {
// Try to load feature-specific config
configPath := fmt.Sprintf("features/%s/%s-test-config.yaml", feature, feature)
if _, err := os.Stat(configPath); err == nil {
v := viper.New()
v.SetConfigFile(configPath)
v.SetConfigType("yaml")
if readErr := v.ReadInConfig(); readErr == nil {
var featureCfg config.Config
if unmarshalErr := v.Unmarshal(&featureCfg); unmarshalErr == nil {
// Set default values if not configured
if featureCfg.Auth.JWTSecret == "" {
featureCfg.Auth.JWTSecret = "default-secret-key-please-change-in-production"
}
if featureCfg.Auth.AdminMasterPassword == "" {
featureCfg.Auth.AdminMasterPassword = "admin123"
}
cfg = &featureCfg
}
}
}
}
// Fallback to default config if feature-specific not available
if cfg == nil {
cfg = createTestConfig(s.port)
}
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Database.Host,
@@ -162,10 +233,10 @@ func (s *Server) initDBConnection() error {
cfg.Database.SSLMode,
)
var err error
s.db, err = sql.Open("postgres", dsn)
if err != nil {
return fmt.Errorf("failed to open database connection: %w", err)
var dbErr error
s.db, dbErr = sql.Open("postgres", dsn)
if dbErr != nil {
return fmt.Errorf("failed to open database connection: %w", dbErr)
}
// Test the connection
@@ -329,31 +400,48 @@ func (s *Server) GetBaseURL() string {
}
func createTestConfig(port int) *config.Config {
// Check if there's a test config file (used by config hot reloading tests)
// If it exists, use it. Otherwise, use default config.
testConfigPath := "test-config.yaml"
if _, err := os.Stat(testConfigPath); err == nil {
// Test config file exists, use it
v := viper.New()
v.SetConfigFile(testConfigPath)
v.SetConfigType("yaml")
// Check for feature-specific config file first
// This supports the new modular BDD test structure
feature := os.Getenv("FEATURE")
var configPaths []string
// Read the test config file
if err := v.ReadInConfig(); err == nil {
var cfg config.Config
if err := v.Unmarshal(&cfg); err == nil {
// Override server port for testing
cfg.Server.Port = port
if feature != "" {
// Feature-specific config takes precedence
configPaths = []string{
fmt.Sprintf("features/%s/%s-test-config.yaml", feature, feature),
"test-config.yaml", // Fallback to legacy config
}
} else {
// When running all features, use legacy config
configPaths = []string{"test-config.yaml"}
}
// Set default auth values if not configured
if cfg.Auth.JWTSecret == "" {
cfg.Auth.JWTSecret = "default-secret-key-please-change-in-production"
// Try each config path in order
for _, configPath := range configPaths {
if _, err := os.Stat(configPath); err == nil {
// Config file exists, use it
v := viper.New()
v.SetConfigFile(configPath)
v.SetConfigType("yaml")
// Read the config file
if err := v.ReadInConfig(); err == nil {
var cfg config.Config
if err := v.Unmarshal(&cfg); err == nil {
// Override server port for testing
cfg.Server.Port = port
// Set default auth values if not configured
if cfg.Auth.JWTSecret == "" {
cfg.Auth.JWTSecret = "default-secret-key-please-change-in-production"
}
if cfg.Auth.AdminMasterPassword == "" {
cfg.Auth.AdminMasterPassword = "admin123"
}
log.Debug().Str("config", configPath).Msg("Using test config file")
return &cfg
}
if cfg.Auth.AdminMasterPassword == "" {
cfg.Auth.AdminMasterPassword = "admin123"
}
return &cfg
}
}
}