🧪 feat: complete BDD implementation with comprehensive documentation
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.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
228
doc/BDD_GUIDE.md
Normal file
228
doc/BDD_GUIDE.md
Normal file
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
42
scripts/run-bdd-tests.sh
Executable file
42
scripts/run-bdd-tests.sh
Executable file
@@ -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
|
||||
Reference in New Issue
Block a user