Cases¶
Experimental
gen-cases and .testspec.yaml are experimental — an exploration of the approach. Schema, API, and behavior may change. Feedback welcome.
Alternatively, install the AI agent skills and let an AI generate cases directly from source code — no spec file required.
cases is the primary content of a spec. Each entry describes one scenario and produces one struct literal in the tests slice.
Schema¶
cases:
- name: <test case name>
description: <natural language scenario description>
fields:
<field_name>: <Go-syntax value>
before:
description: <setup description>
mechanism: <mechanism keyword>
returns:
type: <Go return type>
used_as: <how the returned value is used>
after:
description: <teardown description>
mechanism: <mechanism keyword>
checks:
- <check_id>(<args>)
gates:
<gate_field>: <value>
todo: false | true
Top-level case fields¶
| Field | Required | Description |
|---|---|---|
name |
yes | The string passed to t.Run(). Appears as name: "..." in the struct literal. |
description |
yes | Human-readable scenario description. Precise enough for an AI to generate the correct before code. |
fields |
no | Values for table_fields with role input or state. Keys are field names; values are Go-syntax strings. |
before |
no | Setup hook. See before/after patterns. |
after |
no | Teardown hook. See before/after patterns. |
checks |
no | List of check function call expressions. |
gates |
no | Values for table_fields with role gate. |
todo |
no | true emits a comment placeholder instead of a struct literal. |
name¶
The case name becomes the first field in the struct literal and is the string t.Run() uses.
Generated:
Conventions:
- Use kebab-case:
"json-marshal-error","http-status-code-ok" - Describe the scenario, not the code path:
"returns-user-when-request-is-valid"over"success" - Keep it short enough to read in
go test -voutput
description¶
A natural-language paragraph explaining exactly what this scenario tests. The description is used in two ways:
- As
// ai-hint:comments inside generated stubs — telling the AI what to implement. - As documentation for the human reading the spec.
- name: "json-marshal-error"
description: >
JSON serialization of the payload fails. Deliver must return
a Result with an error without attempting the HTTP request.
Be precise: name the method that fails, the error message returned, and the expected behavior.
fields¶
Values for table_fields with role input or state. The key is the field name; the value is a Go expression string used verbatim in the generated struct literal.
- name: "http-status-code-ok"
fields:
config: '&Config{Endpoint: "http://localhost:8080/webhook", Headers: map[string]string{"Header-XYZ": "xyz"}}'
message: '&model.Notification{Event: model.EventType("test-event"), Data: "test-data"}'
Generated:
{
name: "http-status-code-ok",
config: &Config{Endpoint: "http://localhost:8080/webhook", Headers: map[string]string{"Header-XYZ": "xyz"}},
message: &model.Notification{Event: model.EventType("test-event"), Data: "test-data"},
...
},
For multi-line values, use YAML block scalars:
fields:
notifiers: |
[]model.Notifier{
dummy.New(&dummy.Config{Name: "dummy-01"}),
dummy.New(&dummy.Config{Name: "dummy-02"}),
}
Generated (after go/format):
notifiers: []model.Notifier{
dummy.New(&dummy.Config{Name: "dummy-01"}),
dummy.New(&dummy.Config{Name: "dummy-02"}),
},
Nil values¶
checks¶
A list of check function call expressions. Each string is a Go function call that gets inserted as an argument to the composer.
Generated:
For cross-package check types, gen-cases automatically adds the package qualifier:
Generated (with package: github.com/padiazg/notifier/model):
Empty checks (AI fills in)¶
If checks is absent or empty, gen-cases emits an // ai-hint: comment inside the composer call:
checks: checkEngine(
// ai-hint: add checks for case "success"
// All notifiers connect without error. Engine.Start() must
// produce no errors in the `errors` package variable.
),
Use --no-hints to omit these comments entirely.
gates¶
Values for table_fields with role gate. Gate fields control conditional logic in the test body (if tt.wantPanic, if tt.wantLog != "").
- name: "non-ok-status"
gates:
wantLog: "webhook returned non-OK status: 403"
wantPanic: false
wantValue: true
Generated:
{
name: "non-ok-status",
wantLog: "webhook returned non-OK status: 403",
wantPanic: false,
wantValue: true,
},
fields vs gates
Both fields and gates produce identical struct field assignments. The distinction is semantic. Use fields for function inputs and gates for test-body control signals. If a gate value lives naturally in fields, that works too — gen-cases checks both maps.
todo¶
When todo: true, gen-cases emits a comment block instead of a compilable struct literal.
- name: "partial-failure"
todo: true
description: >
First notifier connects successfully, second fails with a network error.
Engine.Start() must report the second error via OnError and continue.
checks:
- hasErrors(true)
Generated:
// TODO: implement case "partial-failure"
// First notifier connects successfully, second fails with a network error.
// Engine.Start() must report the second error via OnError and continue.
// Suggested checks: hasErrors(true)
Use todo: true to:
- Reserve a slot for a case you know you need but haven't designed yet.
- Mark cases that require complex setup that will be implemented later.
- Generate a readable TODO list from the spec without breaking compilation.
Minimal Case¶
The simplest possible case — no before, no extra fields, no checks yet:
- name: "default"
description: New(nil) creates an Engine with default values.
fields:
config: "nil"
checks:
- hasOnError(false)
- hasNotifiers(0)
Generated:
Case with All Fields¶
- name: "json-marshal-error"
description: >
JSON serialization of the payload fails. Deliver must return
a Result with an error without attempting the HTTP request.
fields:
config: "nil"
before:
description: >
Inject into n.jsonMarshal a function that returns
fmt.Errorf("error from json.Marshal").
mechanism: field-injection
checks:
- CheckResultError("error from json.Marshal")
Generated: