398 lines
13 KiB
Markdown
398 lines
13 KiB
Markdown
---
|
|
name: bdd-testing
|
|
description: Behavior-Driven Development testing for dance-lessons-coach using Godog. Use when creating or running BDD tests, implementing new features with BDD, or validating API endpoints through Gherkin scenarios.
|
|
license: MIT
|
|
metadata:
|
|
author: dance-lessons-coach Team
|
|
version: "1.0.0"
|
|
based-on: pkg/bdd implementation
|
|
---
|
|
|
|
# BDD Testing for dance-lessons-coach
|
|
|
|
Behavior-Driven Development testing framework using Godog for the dance-lessons-coach project. This skill provides comprehensive guidance for creating, running, and maintaining BDD tests that validate API endpoints and system behavior.
|
|
|
|
## Key Concepts
|
|
|
|
### Black Box Testing Principles
|
|
- **External API Only**: Tests interact only through public HTTP endpoints
|
|
- **No Internal Access**: No direct access to database, services, or internal components
|
|
- **Real HTTP Requests**: Actual network calls to verify system behavior
|
|
- **Isolation**: Each scenario runs with fresh client instances
|
|
|
|
### Hybrid In-Process Testing
|
|
- **Real Server Code**: Uses actual server implementation running in-test process
|
|
- **Fixed Port**: Test server runs on port 9191
|
|
- **No External Processes**: Avoids complex process management
|
|
- **Graceful Shutdown**: Proper server lifecycle management
|
|
|
|
## Commands
|
|
|
|
### Run BDD Tests
|
|
|
|
```bash
|
|
go test ./features/...
|
|
```
|
|
|
|
Runs all BDD tests in the features directory using Godog test runner.
|
|
|
|
**Arguments:**
|
|
- None (uses standard Go test infrastructure)
|
|
|
|
### Validate BDD Tests
|
|
|
|
```bash
|
|
./scripts/run-bdd-tests.sh
|
|
```
|
|
|
|
Validates BDD tests and fails if any undefined, pending, or skipped steps are found.
|
|
|
|
**Arguments:**
|
|
- None
|
|
|
|
### Create New Feature
|
|
|
|
```bash
|
|
# Create new feature file
|
|
touch features/<feature_name>.feature
|
|
|
|
# Add Gherkin scenarios
|
|
# Implement step definitions in pkg/bdd/steps/
|
|
```
|
|
|
|
**Arguments:**
|
|
- `feature_name` - Name of the feature (e.g., "greet", "health")
|
|
|
|
## Workflows
|
|
|
|
### Implementing a New BDD Feature
|
|
|
|
1. **Create Feature File**: Define scenarios in Gherkin syntax
|
|
2. **Implement Steps**: Add step definitions following Godog's exact patterns
|
|
3. **Run Tests**: Execute and debug scenarios
|
|
4. **Validate**: Ensure no undefined/pending steps
|
|
5. **Document**: Add feature documentation
|
|
|
|
### Debugging BDD Tests
|
|
|
|
1. **Check Step Patterns**: Ensure steps match Godog's exact regex patterns
|
|
2. **Verify Server**: Confirm test server is running on port 9191
|
|
3. **Inspect Responses**: Check actual vs expected API responses
|
|
4. **Review Logs**: Examine test output for undefined steps
|
|
5. **Validate JSON**: Ensure proper JSON escaping in feature files
|
|
|
|
## Usage Examples
|
|
|
|
### Creating a Greet Feature
|
|
|
|
```gherkin
|
|
# 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 "{\"message\":\"Hello world!\"}"
|
|
|
|
Scenario: Personalized greeting
|
|
Given the server is running
|
|
When I request a greeting for "John"
|
|
Then the response should be "{\"message\":\"Hello John!\"}"
|
|
```
|
|
|
|
### Creating a Health Feature
|
|
|
|
```gherkin
|
|
# features/health.feature
|
|
Feature: Health Endpoint
|
|
The health endpoint should indicate server status
|
|
|
|
Scenario: Health check returns healthy status
|
|
Given the server is running
|
|
When I request the health endpoint
|
|
Then the response should be "{\"status\":\"healthy\"}"
|
|
```
|
|
|
|
### Implementing Step Definitions
|
|
|
|
```go
|
|
// pkg/bdd/steps/steps.go
|
|
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) iRequestAGreetingFor(name string) error {
|
|
return sc.client.Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
|
|
}
|
|
|
|
func (sc *StepContext) iRequestTheDefaultGreeting() error {
|
|
return sc.client.Request("GET", "/api/v1/greet/", nil)
|
|
}
|
|
|
|
func (sc *StepContext) iRequestTheHealthEndpoint() error {
|
|
return sc.client.Request("GET", "/api/health", nil)
|
|
}
|
|
|
|
func (sc *StepContext) theResponseShouldBe(arg1, arg2 string) error {
|
|
// 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)
|
|
}
|
|
```
|
|
|
|
### Registering Steps
|
|
|
|
```go
|
|
// pkg/bdd/steps/steps.go
|
|
func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) {
|
|
sc := NewStepContext(client)
|
|
|
|
// Use Godog's EXACT regex patterns and parameter names
|
|
ctx.Step(`^I request a greeting for "([^"]*)"$`, sc.iRequestAGreetingFor)
|
|
ctx.Step(`^I request the default greeting$`, sc.iRequestTheDefaultGreeting)
|
|
ctx.Step(`^I request the health endpoint$`, sc.iRequestTheHealthEndpoint)
|
|
ctx.Step(`^the response should be "{\"([^"]*)\":\"([^"]*)"}"$`, sc.theResponseShouldBe)
|
|
ctx.Step(`^the server is running$`, sc.theServerIsRunning)
|
|
}
|
|
```
|
|
|
|
## Gotchas
|
|
|
|
### Step Pattern Matching
|
|
- **Use Godog's Exact Patterns**: Step regex must match Godog's suggestions precisely
|
|
- **Use Exact Parameter Names**: Godog expects `arg1, arg2`, not descriptive names
|
|
- **Avoid Undefined Warnings**: Even small deviations cause "undefined step" warnings
|
|
- **Test Patterns First**: Use `godog.ErrPending` to verify patterns work before implementing logic
|
|
- **Don't Over-Optimize Regex**: Use the patterns Godog provides, even if they seem verbose
|
|
|
|
### Critical Requirements from Validated Implementation
|
|
|
|
1. **Godog has very specific requirements** for step pattern matching:
|
|
- Use the **exact regex pattern** that Godog suggests in error messages
|
|
- Use the **exact parameter names** that Godog suggests (`arg1, arg2`, etc.)
|
|
- Match the feature file syntax **exactly** including quotes and JSON formatting
|
|
|
|
2. **The "undefined" warnings are not a Godog bug** - they occur when step definitions don't match Godog's expected patterns exactly:
|
|
- Using different regex patterns than what Godog suggests
|
|
- Using descriptive parameter names instead of `arg1, arg2`
|
|
- Not escaping quotes properly in JSON patterns
|
|
- Trying to be "clever" with regex optimization
|
|
|
|
3. **Solution**: Always use the exact pattern and parameter names that Godog suggests in its error messages.
|
|
|
|
### JSON Escaping
|
|
- **Feature Files**: Use double backslashes for quotes: `"{\\"message\\":\\"Hello\\"}"`
|
|
- **Step Implementation**: Trim surrounding quotes and backslashes from captured groups
|
|
- **Response Validation**: Trim trailing newlines from JSON responses
|
|
|
|
### Server Verification
|
|
- **Actual HTTP Requests**: `theServerIsRunning` must make real HTTP call to `/api/ready`
|
|
- **No Mocking**: Black box testing requires real server verification
|
|
- **Port Conflicts**: Test server runs on fixed port 9191
|
|
|
|
### Context Handling
|
|
- **ScenarioContext vs Context**: Steps receive `*godog.ScenarioContext`, not `context.Context`
|
|
- **Client Access**: Store client in StepContext struct for step access
|
|
- **Fresh Instances**: Each scenario gets new client instance
|
|
|
|
## Best Practices
|
|
|
|
### Step Definition Patterns
|
|
|
|
```go
|
|
// ✅ DO: Use Godog's exact regex patterns and parameter names
|
|
ctx.Step(`^I request a greeting for "([^"]*)"$`, sc.iRequestAGreetingFor)
|
|
ctx.Step(`^the response should be "{\"([^"]*)\":\"([^"]*)"}"$`, sc.theResponseShouldBe)
|
|
|
|
// ❌ DON'T: Use different parameter names or patterns
|
|
ctx.Step(`^I request greeting "(.*)"$`, sc.iRequestAGreetingFor) // Wrong pattern
|
|
ctx.Step(`^the response should be "{\"message\":\"([^"]*)"}"$`, sc.theResponseShouldBe) // Wrong pattern
|
|
```
|
|
|
|
### Validated Step Definition Strategy
|
|
|
|
1. **First eliminate "undefined" warnings** by using Godog's exact suggested patterns
|
|
2. **Return `godog.ErrPending`** initially to confirm pattern matching works
|
|
3. **Then implement actual validation** logic
|
|
4. **One pattern per step type** - Use generic patterns to cover similar steps
|
|
|
|
### Response Validation
|
|
|
|
```go
|
|
// ✅ DO: Trim newlines and properly unescape JSON
|
|
func (c *Client) ExpectResponseBody(expected string) error {
|
|
actual := strings.TrimSuffix(string(c.lastBody), "\n")
|
|
if actual != expected {
|
|
return fmt.Errorf("expected %q, got %q", expected, actual)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ❌ DON'T: Assume exact string matching without cleanup
|
|
func (c *Client) ExpectResponseBody(expected string) error {
|
|
if string(c.lastBody) != expected { // May fail due to newlines
|
|
return fmt.Errorf("mismatch")
|
|
}
|
|
}
|
|
```
|
|
|
|
### Test Server Management
|
|
|
|
```go
|
|
// ✅ DO: Use hybrid in-process testing
|
|
func (s *Server) Start() error {
|
|
// Start real server in same process
|
|
go func() {
|
|
if err := s.httpServer.ListenAndServe(); err != nil {
|
|
log.Error().Err(err).Msg("Test server failed")
|
|
}
|
|
}()
|
|
return s.waitForServerReady()
|
|
}
|
|
|
|
// ❌ DON'T: Use external process management
|
|
func startServer() {
|
|
// Avoid: cmd := exec.Command("go", "run", "./cmd/server")
|
|
}
|
|
```
|
|
|
|
### Response Validation
|
|
|
|
```go
|
|
// ✅ DO: Trim newlines and properly unescape JSON
|
|
func (c *Client) ExpectResponseBody(expected string) error {
|
|
actual := strings.TrimSuffix(string(c.lastBody), "\n")
|
|
if actual != expected {
|
|
return fmt.Errorf("expected %q, got %q", expected, actual)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ❌ DON'T: Assume exact string matching without cleanup
|
|
func (c *Client) ExpectResponseBody(expected string) error {
|
|
if string(c.lastBody) != expected { // May fail due to newlines
|
|
return fmt.Errorf("mismatch")
|
|
}
|
|
}
|
|
```
|
|
|
|
## Progressive Disclosure
|
|
|
|
### Core Instructions (SKILL.md)
|
|
- BDD testing fundamentals
|
|
- Common workflows and patterns
|
|
- Gotchas and best practices
|
|
- Basic troubleshooting
|
|
|
|
### Detailed Reference (references/)
|
|
- **GODOG_PATTERNS.md**: Advanced step pattern examples
|
|
- **TEST_SERVER.md**: Test server implementation details
|
|
- **DEBUGGING.md**: Advanced debugging techniques
|
|
- **EXAMPLES.md**: Complete feature examples
|
|
|
|
### Scripts (scripts/)
|
|
- **run-bdd-tests.sh**: Test validation and execution
|
|
- **debug-steps.sh**: Step pattern debugging
|
|
- **generate-stubs.sh**: Step definition stub generation
|
|
|
|
## Validation
|
|
|
|
### Test Validation Script
|
|
|
|
```bash
|
|
# scripts/run-bdd-tests.sh
|
|
#!/bin/bash
|
|
set -e
|
|
|
|
echo "Running BDD tests..."
|
|
go test ./features/... -v
|
|
|
|
# Fail if any undefined/pending/skipped steps
|
|
echo "Validating test results..."
|
|
if go test ./features/... 2>&1 | grep -q "undefined\|pending\|skipped"; then
|
|
echo "ERROR: Found undefined, pending, or skipped steps"
|
|
exit 1
|
|
fi
|
|
|
|
echo "✓ All BDD tests passed with no undefined steps"
|
|
```
|
|
|
|
### Common Validation Issues
|
|
|
|
| Issue | Cause | Solution |
|
|
|-------|-------|----------|
|
|
| Undefined steps | Step pattern doesn't match Godog's exact regex | Use Godog's suggested pattern |
|
|
| JSON mismatch | Trailing newlines or improper escaping | Trim newlines, properly unescape JSON |
|
|
| Server not running | Test server failed to start | Check port 9191, verify server logs |
|
|
| Context errors | Wrong context type passed to steps | Use `*godog.ScenarioContext`, not `context.Context` |
|
|
|
|
## References
|
|
|
|
- [Godog Documentation](https://github.com/cucumber/godog)
|
|
- [Gherkin Reference](https://cucumber.io/docs/gherkin/)
|
|
- [BDD Best Practices](references/BDD_BEST_PRACTICES.md)
|
|
- [Test Server Implementation](references/TEST_SERVER.md)
|
|
- [Debugging Guide](references/DEBUGGING.md)
|
|
|
|
## Troubleshooting
|
|
|
|
### "Undefined Step" Warnings
|
|
|
|
**Symptoms:** Tests pass but show "undefined step" warnings
|
|
|
|
**Cause:** Step regex doesn't match Godog's exact pattern suggestions
|
|
|
|
**Solution:**
|
|
1. Run `godog --format=progress` to see suggested patterns
|
|
2. Update step registration to use exact patterns
|
|
3. Ensure function names match step descriptions
|
|
|
|
### JSON Comparison Failures
|
|
|
|
**Symptoms:** Response validation fails despite correct JSON
|
|
|
|
**Cause:** Trailing newlines or improper escaping in feature files
|
|
|
|
**Solution:**
|
|
1. Trim newlines: `strings.TrimSuffix(response, "\n")`
|
|
2. Properly escape JSON in feature files: `"{\\"key\\":\\"value\\"}"`
|
|
3. Trim quotes in step implementation: `strings.Trim(arg, `"\`)`
|
|
|
|
### Server Connection Errors
|
|
|
|
**Symptoms:** "connection refused" or server not responding
|
|
|
|
**Cause:** Test server not running or port conflict
|
|
|
|
**Solution:**
|
|
1. Verify server on port 9191: `curl http://localhost:9191/api/ready`
|
|
2. Check server logs for startup errors
|
|
3. Ensure no other process using port 9191
|
|
|
|
### Context Type Mismatches
|
|
|
|
**Symptoms:** Compilation errors about context types
|
|
|
|
**Cause:** Passing wrong context type to step functions
|
|
|
|
**Solution:**
|
|
1. Store `*godog.ScenarioContext` in StepContext
|
|
2. Use stored context for step registration
|
|
3. Access client through StepContext struct
|
|
|
|
## Assets
|
|
|
|
- **feature-template.feature**: Gherkin feature file template
|
|
- **step-template.go**: Go step definition template
|
|
- **test-server-template.go**: Test server implementation template
|
|
- **validation-script.sh**: Test validation script template |