|
| 1 | +# wftest/bdd — Gherkin BDD Support for Workflow Testing |
| 2 | + |
| 3 | +**Date:** 2026-03-23 |
| 4 | +**Status:** Approved |
| 5 | + |
| 6 | +## Overview |
| 7 | + |
| 8 | +Add BDD/Gherkin support to the wftest integration test harness. Pre-built godog step definitions map Gherkin scenarios to wftest harness methods. Pipeline + scenario coverage reporting. Strict mode for CI enforcement of unimplemented features. |
| 9 | + |
| 10 | +## Architecture |
| 11 | + |
| 12 | +``` |
| 13 | +wftest/bdd/ |
| 14 | +├── steps.go # Pre-built godog step definitions |
| 15 | +├── context.go # BDD test context (wraps wftest.Harness per scenario) |
| 16 | +├── coverage.go # Pipeline + scenario coverage calculation |
| 17 | +├── strict.go # Undefined/pending step detection |
| 18 | +├── runner.go # RunFeatures(t, dir, opts...) integration point |
| 19 | +└── runner_test.go |
| 20 | +``` |
| 21 | + |
| 22 | +**Dependency:** `github.com/cucumber/godog v0.15.1` (same version as modular) |
| 23 | + |
| 24 | +## Pre-Built Step Definitions |
| 25 | + |
| 26 | +### Engine Setup |
| 27 | + |
| 28 | +```gherkin |
| 29 | +Given the workflow engine is loaded with "config/app.yaml" |
| 30 | +Given the workflow engine is loaded with config: |
| 31 | + """yaml |
| 32 | + pipelines: |
| 33 | + greet: |
| 34 | + steps: |
| 35 | + - name: say_hello |
| 36 | + type: step.set |
| 37 | + config: |
| 38 | + values: |
| 39 | + message: "hello" |
| 40 | + """ |
| 41 | +``` |
| 42 | + |
| 43 | +### Mocking |
| 44 | + |
| 45 | +```gherkin |
| 46 | +Given step "step.db_query" is mocked to return: |
| 47 | + | key | value | |
| 48 | + | rows | [] | |
| 49 | + | count | 0 | |
| 50 | +Given step "step.db_query" returns JSON: |
| 51 | + """json |
| 52 | + {"rows": [{"id": 1, "email": "test@example.com"}], "count": 1} |
| 53 | + """ |
| 54 | +Given module "database" "db" is mocked |
| 55 | +``` |
| 56 | + |
| 57 | +### HTTP Triggers |
| 58 | + |
| 59 | +```gherkin |
| 60 | +When I POST "/api/v1/auth/register" with JSON: |
| 61 | + """json |
| 62 | + {"email": "test@example.com", "password": "secret123"} |
| 63 | + """ |
| 64 | +When I GET "/api/v1/users/me" with header "Authorization" = "Bearer token123" |
| 65 | +When I PUT "/api/v1/users/123" with: |
| 66 | + | name | Updated Name | |
| 67 | +When I DELETE "/api/v1/items/456" |
| 68 | +``` |
| 69 | + |
| 70 | +### Pipeline Triggers |
| 71 | + |
| 72 | +```gherkin |
| 73 | +When I execute pipeline "process-order" with: |
| 74 | + | order_id | 123 | |
| 75 | + | items | [a, b] | |
| 76 | +When I fire event "user.created" with: |
| 77 | + | user_id | 123 | |
| 78 | +When I fire schedule "daily-cleanup" |
| 79 | +``` |
| 80 | + |
| 81 | +### Response Assertions |
| 82 | + |
| 83 | +```gherkin |
| 84 | +Then the response status should be 201 |
| 85 | +Then the response body should contain "success" |
| 86 | +Then the response JSON "user.id" should not be empty |
| 87 | +Then the response JSON "error" should be "email required" |
| 88 | +Then the response header "Content-Type" should be "application/json" |
| 89 | +``` |
| 90 | + |
| 91 | +### Step Assertions |
| 92 | + |
| 93 | +```gherkin |
| 94 | +Then step "insert_user" should have been executed |
| 95 | +Then step "send_email" should not have been executed |
| 96 | +Then step "calculate_damage" output "damage" should be 8 |
| 97 | +Then step "insert_user" output "rows_affected" should be 1 |
| 98 | +``` |
| 99 | + |
| 100 | +### State Assertions |
| 101 | + |
| 102 | +```gherkin |
| 103 | +Given state "sessions" is seeded from "testdata/combat_setup.json" |
| 104 | +Given state "sessions" has key "game-1" with: |
| 105 | + | players | ["alice", "bob"] | |
| 106 | + | turn | alice | |
| 107 | +Then state "sessions" key "game-1" field "goblin_hp" should be 12 |
| 108 | +Then state "sessions" key "game-1" field "turn" should be "bob" |
| 109 | +``` |
| 110 | + |
| 111 | +### Sequence (Multi-Step Stateful) |
| 112 | + |
| 113 | +```gherkin |
| 114 | +Scenario: Full combat round |
| 115 | + Given the workflow engine is loaded with "config/app.yaml" |
| 116 | + And state "sessions" is seeded from "testdata/combat.json" |
| 117 | +
|
| 118 | + When I execute pipeline "attack" with: |
| 119 | + | game_id | game-1 | |
| 120 | + | attacker | warrior | |
| 121 | + | target | goblin | |
| 122 | + Then step "calculate_damage" output "damage" should be 8 |
| 123 | + And state "sessions" key "game-1" field "goblin_hp" should be 12 |
| 124 | +
|
| 125 | + When I execute pipeline "attack" with: |
| 126 | + | game_id | game-1 | |
| 127 | + | attacker | goblin | |
| 128 | + | target | warrior | |
| 129 | + Then state "sessions" key "game-1" field "warrior_hp" should be 27 |
| 130 | +``` |
| 131 | + |
| 132 | +## Go Integration |
| 133 | + |
| 134 | +```go |
| 135 | +func TestFeatures(t *testing.T) { |
| 136 | + bdd.RunFeatures(t, "features/", |
| 137 | + bdd.WithConfig("config/app.yaml"), |
| 138 | + bdd.WithMockStep("step.db_query", wftest.Returns(defaultDBResponse)), |
| 139 | + ) |
| 140 | +} |
| 141 | + |
| 142 | +func TestAuthFeatures(t *testing.T) { |
| 143 | + bdd.RunFeatures(t, "features/auth.feature", |
| 144 | + bdd.WithConfig("config/app.yaml"), |
| 145 | + ) |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +## Coverage |
| 150 | + |
| 151 | +### Pipeline Coverage |
| 152 | + |
| 153 | +Scans app.yaml for all pipeline names, scans .feature files for pipeline references (explicit `@pipeline:name` tags + implicit HTTP route matching), reports gaps. |
| 154 | + |
| 155 | +``` |
| 156 | +$ wfctl test --coverage config/ |
| 157 | +
|
| 158 | +Pipeline Coverage: 45/78 (57.7%) |
| 159 | +
|
| 160 | +COVERED: |
| 161 | + auth-register .............. auth.feature:12 |
| 162 | + auth-login ................ auth.feature:28 |
| 163 | + wishlist-create ........... wishlists.feature:5 |
| 164 | +
|
| 165 | +UNCOVERED: |
| 166 | + payment-refund |
| 167 | + admin-dispute-update |
| 168 | + cron-expire-claims |
| 169 | +``` |
| 170 | + |
| 171 | +### Scenario Coverage |
| 172 | + |
| 173 | +Counts total scenarios across .feature files, runs them, reports implementation status. |
| 174 | + |
| 175 | +``` |
| 176 | +Scenario Coverage: |
| 177 | + Total: 85 scenarios |
| 178 | + Implemented: 72 (84.7%) |
| 179 | + Passing: 70 (82.4%) |
| 180 | + Pending: 2 (2.4%) |
| 181 | + Undefined: 13 (15.3%) |
| 182 | +``` |
| 183 | + |
| 184 | +## Feature ↔ Pipeline Linking |
| 185 | + |
| 186 | +Two methods: |
| 187 | + |
| 188 | +**Explicit:** `@pipeline:name` tag on scenarios |
| 189 | +```gherkin |
| 190 | +@pipeline:auth-register |
| 191 | +Scenario: Successful registration |
| 192 | + When I POST "/api/v1/auth/register" with JSON: |
| 193 | + ... |
| 194 | +``` |
| 195 | + |
| 196 | +**Implicit:** HTTP route matching — `POST /api/v1/auth/register` matched against pipeline trigger configs in app.yaml. |
| 197 | + |
| 198 | +## Strict Mode |
| 199 | + |
| 200 | +`wfctl test --strict` (or `bdd.RunFeatures(t, ..., bdd.Strict())`) fails on undefined/pending steps: |
| 201 | + |
| 202 | +``` |
| 203 | +$ wfctl test --strict features/ |
| 204 | +
|
| 205 | +FAIL: 3 scenarios have undefined steps: |
| 206 | + auth.feature:23 - Given the user has MFA enabled |
| 207 | + payment.feature:45 - When the webhook signature is invalid |
| 208 | + admin.feature:12 - Then the audit log should contain an entry |
| 209 | +
|
| 210 | +Exit code: 1 |
| 211 | +``` |
| 212 | + |
| 213 | +Default is lenient — undefined steps warn but don't fail. Allows incremental development. |
| 214 | + |
| 215 | +## Implementation Phases |
| 216 | + |
| 217 | +### Phase 1: Core BDD Runner + Step Definitions |
| 218 | +- wftest/bdd package with godog dependency |
| 219 | +- context.go — BDD context wrapping wftest.Harness |
| 220 | +- steps.go — all pre-built step definitions (engine setup, mocking, HTTP, pipeline, response, step, state assertions) |
| 221 | +- runner.go — RunFeatures(t, path, opts...) |
| 222 | +- Test with sample .feature file |
| 223 | + |
| 224 | +### Phase 2: Coverage |
| 225 | +- coverage.go — pipeline coverage (scan app.yaml + feature files) |
| 226 | +- coverage.go — scenario coverage (count defined vs implemented vs passing) |
| 227 | +- wfctl test --coverage integration |
| 228 | + |
| 229 | +### Phase 3: Strict Mode + CLI |
| 230 | +- strict.go — undefined/pending step detection |
| 231 | +- wfctl test --strict integration |
| 232 | +- Update docs/testing.md with BDD section |
0 commit comments