From 85b6cf82ee4604377adc94e07b49dc38983e783a Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Sat, 4 Apr 2026 17:59:07 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20feat:=20complete=20BDD=20impleme?= =?UTF-8?q?ntation=20with=20comprehensive=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finalize BDD testing framework with: - Unified step definitions using StepContext struct - Proper server verification in theServerIsRunning step - Robust JSON response handling with escaping and newline trimming - Updated documentation reflecting current implementation - Test validation script to ensure test quality - All tests passing with proper black box testing Key files updated: - pkg/bdd/steps/steps.go: Unified step definitions - pkg/bdd/testserver/client.go: Robust response validation - pkg/bdd/README.md: Godog pattern guide - doc/BDD_GUIDE.md: Updated usage guide - adr/0008-bdd-testing.md: Updated ADR with current approach - scripts/run-bdd-tests.sh: Test validation script The BDD framework is now production-ready with comprehensive documentation and proper testing practices. --- adr/0008-bdd-testing.md | 42 ++++--- doc/BDD_GUIDE.md | 228 +++++++++++++++++++++++++++++++++++ pkg/bdd/steps/steps.go | 24 ++-- pkg/bdd/testserver/client.go | 13 ++ scripts/run-bdd-tests.sh | 42 +++++++ 5 files changed, 328 insertions(+), 21 deletions(-) create mode 100644 doc/BDD_GUIDE.md create mode 100755 scripts/run-bdd-tests.sh diff --git a/adr/0008-bdd-testing.md b/adr/0008-bdd-testing.md index 715515a..e042155 100644 --- a/adr/0008-bdd-testing.md +++ b/adr/0008-bdd-testing.md @@ -171,23 +171,37 @@ Feature: Greet Service ## Example Step Implementation ```go -// 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() - }) +// pkg/bdd/steps/steps.go +func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) { + sc := NewStepContext(client) - ctx.Step(`^I request the default greeting$`, func() error { - return getCurrentClient(defaultClient).Request("GET", "/api/v1/greet/", nil) - }) + ctx.Step(`^the server is running$`, sc.theServerIsRunning) + ctx.Step(`^I request the default greeting$`, sc.iRequestTheDefaultGreeting) + ctx.Step(`^I request a greeting for "([^"]*)"$`, sc.iRequestAGreetingFor) + ctx.Step(`^I request the health endpoint$`, sc.iRequestTheHealthEndpoint) + ctx.Step(`^the response should be "{\"([^"]*)\":\"([^"]*)\"}"$`, sc.theResponseShouldBe) +} - ctx.Step(`^I request a greeting for "([^"]*)"$`, func(name string) error { - return getCurrentClient(defaultClient).Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil) - }) +// StepContext struct holds the test client +type StepContext struct { + client *testserver.Client +} - ctx.Step(`^the response should be "([^"]*)"$`, func(expected string) error { - return getCurrentClient(defaultClient).ExpectResponseBody(expected) - }) +func (sc *StepContext) theServerIsRunning() error { + // Actually verify the server is running by checking the readiness endpoint + return sc.client.Request("GET", "/api/ready", nil) +} + +func (sc *StepContext) iRequestTheDefaultGreeting() error { + return sc.client.Request("GET", "/api/v1/greet/", nil) +} + +func (sc *StepContext) theResponseShouldBe(arg1, arg2 string) error { + // Handle JSON escaping from feature files + cleanArg1 := strings.Trim(arg1, `"\`) + cleanArg2 := strings.Trim(arg2, `"\`) + expected := fmt.Sprintf(`{"%s":"%s"}`, cleanArg1, cleanArg2) + return sc.client.ExpectResponseBody(expected) } ``` diff --git a/doc/BDD_GUIDE.md b/doc/BDD_GUIDE.md new file mode 100644 index 0000000..d9354d3 --- /dev/null +++ b/doc/BDD_GUIDE.md @@ -0,0 +1,228 @@ +# BDD Testing Guide for DanceLessonsCoach + +This guide explains how to work with BDD tests using Godog in the DanceLessonsCoach project. + +## Installation + +### Modern Go Approach (Recommended) + +The idiomatic and modern way to install Godog for developers joining the project: + +```bash +# Install Godog as a Go tool (recommended approach) +go install github.com/cucumber/godog/cmd/godog@latest +``` + +This installs the `godog` binary in your `$GOPATH/bin` directory. Make sure this directory is in your `PATH`. + +### Alternative: Using go run + +You can also run Godog directly without installing: + +```bash +go run github.com/cucumber/godog/cmd/godog@latest +``` + +### Project Setup + +The project already includes Godog as a dependency in `go.mod`. The BDD tests are integrated into the standard Go testing framework. + +## Running BDD Tests + +### Run All BDD Tests + +```bash +# From project root +cd /Users/gabrielradureau/Work/Vibe/DanceLessonsCoach +go test ./features/... -v +``` + +### Run Specific Feature + +```bash +# Run only greet feature +go test ./features/... -v -run "TestBDD/Greet" + +# Run only health feature +go test ./features/... -v -run "TestBDD/Health" + +# Run only readiness feature +go test ./features/... -v -run "TestBDD/Readiness" +``` + +### Different Output Formats + +```bash +# Progress format (default) +go test ./features/... -v + +# Pretty format +go test ./features/... -v -godog.format=pretty + +# JSON format +go test ./features/... -v -godog.format=json + +# HTML report +go test ./features/... -v -godog.format=html > report.html +``` + +## Project Structure + +``` +features/ +├── bdd_test.go # Main test entry point +├── greet.feature # Greet service feature +└── health.feature # Health endpoint feature + +pkg/bdd/ +├── steps/ # Step definitions +│ └── steps.go # All step definitions +│ +├── testserver/ # Test infrastructure +│ ├── server.go # Test server management +│ └── client.go # HTTP client for testing +│ +├── suite.go # Test suite initialization +└── README.md # BDD usage guide +``` + +## Writing New Features + +### 1. Create Feature File + +Create a new `.feature` file in the `features/` directory using Gherkin syntax: + +```gherkin +# features/new_feature.feature +Feature: New Feature + Description of the new feature + + Scenario: Happy path + Given some precondition + When I perform an action + Then I should see the expected result +``` + +### 2. Implement Step Definitions + +Create a corresponding step definition file in `pkg/bdd/steps/`: + +```go +// pkg/bdd/steps/new_feature_steps.go +package steps + +import ( + "DanceLessonsCoach/pkg/bdd/testserver" + "github.com/cucumber/godog" +) + +func InitializeNewFeatureSteps(ctx *godog.ScenarioContext, client *testserver.Client) { + ctx.Step(`^some precondition$`, func() error { + // Implementation + return nil + }) + + ctx.Step(`^I perform an action$`, func() error { + // Implementation + return nil + }) + + ctx.Step(`^I should see the expected result$`, func() error { + // Implementation + return nil + }) +} +``` + +### 3. Register Steps + +Add your step initialization to `pkg/bdd/suite.go`: + +```go +func InitializeScenario(ctx *godog.ScenarioContext) { + server := testserver.NewServer() + client := testserver.NewClient(server) + + // Initialize all steps using the unified approach + steps.InitializeAllSteps(ctx, client) + + // ... cleanup code +} +``` + +## Best Practices + +### Test Organization + +- **One feature per file**: Keep feature files focused on a single capability +- **Clear scenario names**: Use descriptive scenario names that explain the behavior +- **Reusable steps**: Create reusable step definitions when possible +- **Black box testing**: Test through public APIs only, no internal knowledge + +### Performance + +- **Parallel execution**: Godog supports parallel scenario execution +- **Isolated scenarios**: Each scenario should be independent +- **Cleanup**: Always cleanup resources in `After` hooks + +### Debugging + +- **Verbose output**: Use `-v` flag for detailed output +- **Step-by-step**: Run with `-godog.tags=@focus` to run specific scenarios +- **Dry run**: Check step definitions without running tests + +## Troubleshooting + +### Server not starting + +If tests fail with "server did not become ready", check: +- Port conflicts: `lsof -i :8080` +- Server logs: Check if server process starts properly +- Configuration: Verify config.yaml settings + +### Missing step definitions + +If you see "undefined steps", you need to: +1. Implement the missing step definitions +2. Register them in the suite initialization +3. Re-run the tests + +### Test isolation issues + +Ensure each scenario: +- Starts with a clean state +- Doesn't depend on other scenarios +- Properly cleans up resources + +## CI/CD Integration + +Add BDD tests to your CI pipeline: + +```yaml +# Example GitHub Actions step +- name: Run BDD Tests + run: go test ./features/... -v +``` + +## Additional Resources + +- [Godog GitHub](https://github.com/cucumber/godog) +- [Godog Documentation](https://github.com/cucumber/godog#readme) +- [Cucumber Documentation](https://cucumber.io/docs/gherkin/) +- [BDD Introduction](https://dannorth.net/introducing-bdd/) + +## Modern Go Testing Practices + +The DanceLessonsCoach project follows modern Go testing practices: + +1. **Standard library integration**: BDD tests use `go test` +2. **No global installation required**: Godog is a Go module dependency +3. **Cross-platform**: Works on all Go-supported platforms +4. **IDE integration**: Works with Go tools and IDEs +5. **Dependency management**: Versioned through go.mod + +This approach ensures that: +- All developers use the same Godog version +- No manual installation steps are needed +- Tests work consistently across environments +- CI/CD integration is straightforward \ No newline at end of file diff --git a/pkg/bdd/steps/steps.go b/pkg/bdd/steps/steps.go index 691202c..b67ddaa 100644 --- a/pkg/bdd/steps/steps.go +++ b/pkg/bdd/steps/steps.go @@ -3,6 +3,7 @@ package steps import ( "DanceLessonsCoach/pkg/bdd/testserver" "fmt" + "strings" "github.com/cucumber/godog" ) @@ -20,7 +21,6 @@ func NewStepContext(client *testserver.Client) *StepContext { // InitializeAllSteps registers all step definitions for the BDD tests func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) { sc := NewStepContext(client) - fmt.Println("DEBUG: InitializeAllSteps called") ctx.Step(`^I request a greeting for "([^"]*)"$`, sc.iRequestAGreetingFor) ctx.Step(`^I request the default greeting$`, sc.iRequestTheDefaultGreeting) @@ -29,23 +29,33 @@ func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) { ctx.Step(`^the server is running$`, sc.theServerIsRunning) } -func (sc *StepContext) iRequestAGreetingFor(arg1 string) error { - return godog.ErrPending +func (sc *StepContext) iRequestAGreetingFor(name string) error { + return sc.client.Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil) } func (sc *StepContext) iRequestTheDefaultGreeting() error { - return godog.ErrPending + return sc.client.Request("GET", "/api/v1/greet/", nil) } func (sc *StepContext) iRequestTheHealthEndpoint() error { - return godog.ErrPending + return sc.client.Request("GET", "/api/health", nil) } func (sc *StepContext) theResponseShouldBe(arg1, arg2 string) error { - expected := fmt.Sprintf(`{"%s":"%s"}`, arg1, arg2) + // The regex captures the full JSON from the feature file, including quotes + // We need to extract just the key and value without the surrounding quotes and backslashes + + // Remove the surrounding quotes and backslashes + cleanArg1 := strings.Trim(arg1, `"\`) + cleanArg2 := strings.Trim(arg2, `"\`) + + // Build the expected JSON string + expected := fmt.Sprintf(`{"%s":"%s"}`, cleanArg1, cleanArg2) + return sc.client.ExpectResponseBody(expected) } func (sc *StepContext) theServerIsRunning() error { - return godog.ErrPending + // Actually verify the server is running by checking the readiness endpoint + return sc.client.Request("GET", "/api/ready", nil) } diff --git a/pkg/bdd/testserver/client.go b/pkg/bdd/testserver/client.go index d77b7a4..55d4ff4 100644 --- a/pkg/bdd/testserver/client.go +++ b/pkg/bdd/testserver/client.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "strings" ) type Client struct { @@ -46,9 +47,21 @@ func (c *Client) ExpectResponseBody(expected string) error { } actual := string(c.lastBody) + // Trim trailing newline if present (common in JSON responses) + actual = strings.TrimSuffix(actual, "\n") + if actual != expected { return fmt.Errorf("expected response body %q, got %q", expected, actual) } return nil } + +// Helper methods for debugging +func (c *Client) GetLastResponse() *http.Response { + return c.lastResp +} + +func (c *Client) GetLastBody() []byte { + return c.lastBody +} diff --git a/scripts/run-bdd-tests.sh b/scripts/run-bdd-tests.sh new file mode 100755 index 0000000..1d90fbb --- /dev/null +++ b/scripts/run-bdd-tests.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# BDD Test Runner Script +# Runs all BDD tests and fails if there are undefined, pending, or skipped steps + +set -e + +echo "🧪 Running BDD Tests..." +cd /Users/gabrielradureau/Work/Vibe/DanceLessonsCoach + +# Run the BDD tests +test_output=$(go test ./features/... -v 2>&1) +test_exit_code=$? + +echo "$test_output" + +# Check for undefined steps +if echo "$test_output" | grep -q "undefined"; then + echo "❌ FAILED: Found undefined steps" + exit 1 +fi + +# Check for pending steps +if echo "$test_output" | grep -q "pending"; then + echo "❌ FAILED: Found pending steps" + exit 1 +fi + +# Check for skipped steps +if echo "$test_output" | grep -q "skipped"; then + echo "❌ FAILED: Found skipped steps" + exit 1 +fi + +# Check if tests passed +if [ $test_exit_code -eq 0 ]; then + echo "✅ All BDD tests passed successfully!" + exit 0 +else + echo "❌ BDD tests failed" + exit 1 +fi