feat: implement BDD testing with Godog

Implement comprehensive BDD testing framework using Godog:
- Added feature files for greet and health endpoints
- Created test server that runs on port 9191
- Implemented step definitions using Godog's exact patterns
- Fixed undefined step warnings by following Godog conventions
- All tests passing with proper response validation
- Maintained black box testing principles

Key files:
- pkg/bdd/steps/steps.go - Step definitions using StepContext struct
- pkg/bdd/testserver/ - Test server implementation
- features/*.feature - BDD feature files
- pkg/bdd/README.md - Documentation for proper step patterns

The implementation follows Godog's exact pattern suggestions to avoid
undefined step warnings and provides comprehensive API testing.
This commit is contained in:
2026-04-04 17:43:57 +02:00
parent 95596b5e12
commit 0daaf9bf96
11 changed files with 857 additions and 16 deletions

View File

@@ -83,12 +83,73 @@ pkg/bdd/
│ └── readiness_steps.go
├── testserver/ # Test infrastructure
│ ├── server.go # Test server management
│ ├── server.go # In-process test server harness
│ └── client.go # HTTP client for testing
└── suite.go # Test suite initialization
```
## Testing Approach Evolution
### Initial Approach (Process-based)
Initially planned to test against external server process using `go run`, but this proved unreliable for automated testing due to:
- Process management complexity
- Port conflicts in parallel execution
- CI/CD environment challenges
- Process cleanup issues
### Current Approach (Hybrid In-Process)
Adopted a hybrid approach that maintains black box testing principles while improving reliability:
```go
// pkg/bdd/testserver/server.go
func (s *Server) Start() error {
// Create real server instance from pkg/server
cfg := createTestConfig(s.port)
realServer := server.NewServer(cfg, context.Background())
// Start HTTP server in same process
s.httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Handler: realServer.Router(),
}
go func() {
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error().Err(err).Msg("Test server failed")
}
}()
return s.waitForServerReady()
}
```
## Black Box Testing Principles Maintained
Despite using in-process server, the approach maintains core black box testing principles:
**External Interface Testing**: All tests interact through HTTP API only
**No Implementation Knowledge**: Tests don't access internal server components
**Real Server Code**: Uses actual server implementation from `pkg/server`
**Production Configuration**: Tests with realistic server configuration
**Isolation**: Each test suite gets fresh server instance
## What We Test vs What We Don't
### ✅ Covered by BDD Tests
- HTTP API endpoints and responses
- Request/response handling
- Business logic through public interface
- Error handling and status codes
- Readiness/liveness behavior
- JSON serialization/deserialization
### 🚫 Not Covered by BDD Tests (Covered Elsewhere)
- Actual process startup/shutdown (covered by `scripts/test-server.sh`)
- Main function execution (covered by integration tests)
- External process management (covered by server control scripts)
- Operating system signals (covered by manual testing)
## Example Feature File
```gherkin
@@ -111,21 +172,21 @@ Feature: Greet Service
```go
// pkg/bdd/steps/greet_steps.go
func InitializeGreetSteps(ctx *godog.ScenarioContext, client *testserver.Client) {
func InitializeGreetSteps(ctx *godog.ScenarioContext, defaultClient *testserver.Client) {
ctx.Step(`^the server is running$`, func() error {
return client.Start()
return getCurrentClient(defaultClient).Start()
})
ctx.Step(`^I request the default greeting$`, func() error {
return client.Request("GET", "/api/v1/greet/", nil)
return getCurrentClient(defaultClient).Request("GET", "/api/v1/greet/", nil)
})
ctx.Step(`^I request a greeting for "([^"]*)"$`, func(name string) error {
return client.Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
return getCurrentClient(defaultClient).Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
})
ctx.Step(`^the response should be "([^"]*)"$`, func(expected string) error {
return client.ExpectResponseBody(expected)
return getCurrentClient(defaultClient).ExpectResponseBody(expected)
})
}
```