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:
@@ -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)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user