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.
7.3 KiB
Adopt BDD with Godog for behavioral testing
- Status: Accepted
- Deciders: Gabriel Radureau, AI Agent
- Date: 2026-04-05
Context and Problem Statement
We needed to add behavioral testing to DanceLessonsCoach that provides:
- User-centric test scenarios
- Living documentation
- Integration testing capabilities
- Clear communication between technical and non-technical stakeholders
- Complementary testing to unit tests
Decision Drivers
- Need for higher-level testing than unit tests
- Desire for living documentation that's always up-to-date
- Requirement for testing through public interfaces
- Need for clear behavioral specifications
- Desire for good test organization and readability
Considered Options
- Godog (Cucumber for Go) - BDD framework for Go
- Ginkgo - BDD-style testing framework
- Standard Go testing - Extended for integration tests
- Custom BDD framework - Build our own
Decision Outcome
Chosen option: "Godog" because it provides proper BDD support with Gherkin syntax, good Go integration, living documentation capabilities, and follows standard Cucumber patterns.
Pros and Cons of the Options
Godog
- Good, because proper BDD with Gherkin syntax
- Good, because living documentation
- Good, because good Go integration
- Good, because follows Cucumber standards
- Good, because clear separation of concerns
- Bad, because slightly more complex setup
- Bad, because slower execution than unit tests
Ginkgo
- Good, because good BDD-style testing
- Good, because fast execution
- Good, because good Go integration
- Bad, because not proper Gherkin/BDD
- Bad, because less clear for non-technical stakeholders
Standard Go testing
- Good, because no external dependencies
- Good, because familiar to Go developers
- Bad, because no BDD capabilities
- Bad, because no living documentation
- Bad, because less organized for behavioral tests
Custom BDD framework
- Good, because tailored to our needs
- Good, because no external dependencies
- Bad, because time-consuming to develop
- Bad, because need to maintain ourselves
- Bad, because likely less feature-rich
Implementation Structure
features/
├── greet.feature # Gherkin feature files
├── health.feature
└── readiness.feature
pkg/bdd/
├── steps/ # Step definitions
│ ├── greet_steps.go # Implementation of steps
│ ├── health_steps.go
│ └── readiness_steps.go
│
├── testserver/ # Test infrastructure
│ ├── 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:
// 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
# features/greet.feature
Feature: Greet Service
The greet service should return appropriate greetings
Scenario: Default greeting
Given the server is running
When I request the default greeting
Then the response should be "Hello world!"
Scenario: Personalized greeting
Given the server is running
When I request a greeting for "John"
Then the response should be "Hello John!"
Example Step Implementation
// pkg/bdd/steps/greet_steps.go
func InitializeGreetSteps(ctx *godog.ScenarioContext, defaultClient *testserver.Client) {
ctx.Step(`^the server is running$`, func() error {
return getCurrentClient(defaultClient).Start()
})
ctx.Step(`^I request the default greeting$`, func() error {
return getCurrentClient(defaultClient).Request("GET", "/api/v1/greet/", nil)
})
ctx.Step(`^I request a greeting for "([^"]*)"$`, func(name string) error {
return getCurrentClient(defaultClient).Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
})
ctx.Step(`^the response should be "([^"]*)"$`, func(expected string) error {
return getCurrentClient(defaultClient).ExpectResponseBody(expected)
})
}
Black Box Testing Approach
The BDD implementation follows black box testing principles:
- External perspective: Tests interact only through public HTTP API
- No implementation knowledge: Tests don't know about internal components
- Behavior focus: Tests verify what the system does, not how it does it
- Interface testing: Tests verify the contract between system and users
Testing Strategy
Test Types
- Direct HTTP tests: Test raw API behavior
- SDK client tests: Test generated client integration (future)
Test Execution
# Run BDD tests
cd features
godog
# Run with specific format
godog -f progress
# Run specific feature
godog features/greet.feature
Links
Integration with CI/CD
# Example GitHub Actions step
- name: Run BDD tests
run: |
cd features
godog -f progress
Performance Considerations
- BDD tests are slower than unit tests (expected)
- Each scenario runs with fresh server instance for isolation
- Tests can be run in parallel where appropriate
- Focus on critical paths rather than exhaustive testing