Complete Examples¶
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.
Three worked examples covering the most common patterns: global checks with package state, cross-package check types, and state-mutation with a returning before.
Engine.Start — global checks + package state¶
Tests a method that starts goroutines and reports errors via a callback. Uses package-level state to capture errors.
Spec¶
version: "1"
package: ./engine
function: Engine.Start
context:
subject_init: |
c := &Config{OnError: registerError}
e := New(c)
shared_setup: >
Call clearErrors() before each case.
Register each notifier from tt.notifiers via e.Register(n).
Call e.Start(), time.Sleep(250ms), e.Stop(), time.Sleep(250ms).
package_state:
- name: errors
type: "[]error"
clear_between_cases: true
description: >
Accumulates errors reported by the Config.OnError callback (registerError).
Verified with hasErrors().
table_fields:
- name: notifiers
type: "[]model.Notifier"
role: input
doc: >
Notifiers to register. DummyNotifier.Config.ConnectError controls
whether Connect() fails.
check_types:
- id: engine_check
type_name: engineTestCheckFn
signature: "func(*testing.T, *Engine)"
composer: checkEngine
description: Validates the engine state after Start()/Stop().
checks:
- id: hasErrors
for_type: engine_check
scope: global
signature: "hasErrors(has bool) engineTestCheckFn"
when: >
Verify whether the package-level `errors` variable has entries.
Use has=true when a notifier fails in Connect().
Use has=false when all notifiers connect correctly.
params:
- name: has
type: bool
doc: "true = expects at least one error; false = expects empty slice"
cases:
- name: "connect-error"
description: >
A notifier with ConnectError configured fails to connect.
Engine.Start() must report the error via OnError without panicking
or blocking other notifiers.
fields:
notifiers: |
[]model.Notifier{
&dummy.DummyNotifier{
Config: &dummy.Config{
Name: "dummy-01",
ConnectError: fmt.Errorf("connecting"),
},
},
}
checks:
- hasErrors(true)
- name: "success"
description: >
A notifier without ConnectError connects successfully.
Engine.Start() must produce no errors.
fields:
notifiers: |
[]model.Notifier{
&dummy.DummyNotifier{
Config: &dummy.Config{Name: "dummy-01"},
},
}
checks:
- hasErrors(false)
Generated output¶
func TestEngine_Start(t *testing.T) {
tests := []struct {
name string
notifiers []model.Notifier
checks []engineTestCheckFn
}{
{
name: "connect-error",
notifiers: []model.Notifier{
&dummy.DummyNotifier{
Config: &dummy.Config{
Name: "dummy-01",
ConnectError: fmt.Errorf("connecting"),
},
},
},
checks: checkEngine(
hasErrors(true),
),
},
{
name: "success",
notifiers: []model.Notifier{
&dummy.DummyNotifier{
Config: &dummy.Config{Name: "dummy-01"},
},
},
checks: checkEngine(
hasErrors(false),
),
},
}
// ... test body (not modified by gen-cases)
}
WebhookNotifier.Deliver — cross-package checks + field injection¶
Tests a method that calls external HTTP. Uses a cross-package check type and field injection to replace internal functions.
Spec¶
version: "1"
package: ./connector/webhook
function: WebhookNotifier.Deliver
context:
subject_init: "n := New(tt.config)"
table_fields:
- name: config
type: "*Config"
role: input
doc: Config for the WebhookNotifier. nil uses defaults.
- name: message
type: "*model.Notification"
role: input
doc: Notification to deliver.
check_types:
- id: result_check
type_name: TestCheckResultFn
signature: "func(*testing.T, model.Notifier, *model.Result)"
composer: CheckResult
package: github.com/padiazg/notifier/model
description: Validates the *model.Result returned by Deliver.
checks:
- id: CheckResultError
for_type: result_check
scope: global
signature: "model.CheckResultError(want string) model.TestCheckResultFn"
when: >
Use in all Deliver cases to validate result.Error.
want="" validates success (result.Error == nil).
want="substring" validates that result.Error.Error() contains that substring.
params:
- name: want
type: string
doc: Expected error substring.
sentinel_empty: "empty string asserts result.Error is nil (success)"
cases:
- 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
nil, fmt.Errorf("error from json.Marshal").
mechanism: field-injection
checks:
- CheckResultError("error from json.Marshal")
- name: "http-newrequest-error"
description: >
http.NewRequest fails. Deliver must return a Result with an error
before executing the request.
fields:
config: "nil"
before:
description: >
Inject into n.httpNewRequest a function that returns
nil, fmt.Errorf("test error on http.NewRequest").
mechanism: field-injection
checks:
- CheckResultError("test error on http.NewRequest")
- name: "client-do-error"
description: The HTTP client fails to execute the request. Deliver propagates the error.
before:
description: >
Assign to n.client a mockHTTPClient whose DoFunc returns
nil, errors.New("test http new request error").
mechanism: field-injection
checks:
- CheckResultError("test http new request error")
- name: "http-status-code-not-ok"
description: >
The endpoint returns HTTP 403 Forbidden. Deliver must interpret
the non-OK status as an error and return it in the Result.
fields:
config: '&Config{Endpoint: "http://localhost:8080/webhook"}'
message: '&model.Notification{Event: model.EventType("test-event"), Data: "test-data"}'
before:
description: >
Assign to n.client a mockHTTPClient whose DoFunc returns
&http.Response{StatusCode: http.StatusForbidden, Body: io.NopCloser(...)}
with body string "Forbidden", and nil error.
mechanism: field-injection
checks:
- CheckResultError("webhook returned non-OK status: 403")
- name: "http-status-code-ok"
description: >
The endpoint returns HTTP 200 OK. Deliver completes successfully.
The Result must not contain an error.
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"}'
before:
description: >
Assign to n.client a mockHTTPClient whose DoFunc returns
&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(...)}
with body string "Ok", and nil error.
mechanism: field-injection
checks:
- CheckResultError("")
Generated output (excerpt)¶
{
name: "json-marshal-error",
config: nil,
before: func(n *WebhookNotifier) {
// ai-hint: field-injection
// Inject into n.jsonMarshal a function that returns
// nil, fmt.Errorf("error from json.Marshal").
},
checks: model.CheckResult(
model.CheckResultError("error from json.Marshal"),
),
},
{
name: "http-status-code-not-ok",
config: &Config{Endpoint: "http://localhost:8080/webhook"},
message: &model.Notification{Event: model.EventType("test-event"), Data: "test-data"},
before: func(n *WebhookNotifier) {
// ai-hint: field-injection
// Assign to n.client a mockHTTPClient whose DoFunc returns
// &http.Response{StatusCode: http.StatusForbidden, Body: io.NopCloser(...)}
// with body string "Forbidden", and nil error.
},
checks: model.CheckResult(
model.CheckResultError("webhook returned non-OK status: 403"),
),
},
{
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"},
before: func(n *WebhookNotifier) {
// ai-hint: field-injection
// Assign to n.client a mockHTTPClient whose DoFunc returns
// &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(...)}
// with body string "Ok", and nil error.
},
checks: model.CheckResult(
model.CheckResultError(""),
),
},
DummyNotifier.Exists — state-mutation with returning before¶
Tests a method that reads from internal state. Uses before as a table_field (not the standard struct field) with a return value — the notification prepared by setup is passed directly to Exists().
Spec¶
version: "1"
package: ./connector/dummy
function: DummyNotifier.Exists
context:
subject_init: "d := New(&Config{})"
table_fields:
- name: before
type: "func(d *DummyNotifier) *model.Notification"
role: state
doc: >
Prepares internal state of d and returns the notification to pass
to Exists(). The signature returns *model.Notification because the
search value depends on setup.
- name: want
type: bool
role: gate
doc: Expected result of d.Exists(n).
cases:
- name: "found"
description: >
The notification is in d.in (inserted manually with lock).
Exists() must return true.
fields:
want: "true"
before:
description: >
Create a *model.Notification with ID="payload-01", Event="test", Data=nil.
Acquire d.lock, append the notification to d.in, release lock.
Return the same notification so the test can pass it to Exists().
mechanism: state-mutation
returns:
type: "*model.Notification"
used_as: "argument n passed to d.Exists(n) in the test body"
- name: "not-found"
description: >
The notification is NOT in d.in — d.in is empty.
Exists() must return false.
fields:
want: "false"
before:
description: >
Create a *model.Notification with ID="payload-01", Event="test", Data=nil
but do NOT append it to d.in. Return the notification so the test
can pass it to Exists() and verify it is not found.
mechanism: state-mutation
returns:
type: "*model.Notification"
used_as: "argument n passed to d.Exists(n) in the test body"
Generated output¶
func TestDummyNotifier_Exists(t *testing.T) {
tests := []struct {
name string
before func(d *DummyNotifier) *model.Notification
want bool
}{
{
name: "found",
before: func(d *DummyNotifier) *model.Notification {
// ai-hint: state-mutation
// Create a *model.Notification with ID="payload-01", Event="test", Data=nil.
// Acquire d.lock, append the notification to d.in, release lock.
// Return the same notification so the test can pass it to Exists().
return nil // ai-hint: return the value described above
},
want: true,
},
{
name: "not-found",
before: func(d *DummyNotifier) *model.Notification {
// ai-hint: state-mutation
// Create a *model.Notification with ID="payload-01", Event="test", Data=nil
// but do NOT append it to d.in. Return the notification so the test
// can pass it to Exists() and verify it is not found.
return nil // ai-hint: return the value described above
},
want: false,
},
}
// ... test body (not modified by gen-cases)
}
WebhookNotifier.getClient — local checks + multiple before mechanisms¶
Tests a private method that lazily builds an HTTP client. Uses local checks (defined inside TestXxx) and combines field-injection and field-reset patterns.
Spec (excerpt)¶
version: "1"
package: ./connector/webhook
function: WebhookNotifier.getClient
context:
subject_init: "n := New(tt.config)"
table_fields:
- name: config
type: "*Config"
role: input
doc: Config for the WebhookNotifier.
- name: before
type: "func(*WebhookNotifier)"
role: state
doc: Setup before calling getClient().
check_types:
- id: notifier_check
type_name: TestCheckNotifierFn
signature: "func(*testing.T, model.Notifier)"
composer: CheckNotifier
package: github.com/padiazg/notifier/model
checks:
- id: checkClientType
for_type: notifier_check
scope: local
signature: "checkClientType(clientType interface{}) model.TestCheckNotifierFn"
when: Validate that n.client is of the expected type using reflect.TypeOf.
- id: checkClientInsecure
for_type: notifier_check
scope: local
signature: "checkClientInsecure(want bool) model.TestCheckNotifierFn"
when: >
Validate n.client.(*http.Client).Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify.
Only apply when checkClientType(&http.Client{}) is also present.
cases:
- name: "existing-client"
description: >
If n.client is already assigned (a mock), getClient() must return it as-is
without constructing a new one.
before:
description: Assign n.client = &mockHTTPClient{} before calling getClient().
mechanism: field-injection
checks:
- checkClientType(&mockHTTPClient{})
- name: "empty-client"
description: >
If n.client is nil, getClient() builds a standard *http.Client
with no TLS configuration (InsecureSkipVerify=false).
before:
description: Force n.client = nil to ensure getClient() constructs a new one.
mechanism: field-reset
checks:
- checkClientType(&http.Client{})
- checkClientInsecure(false)
- name: "insecure-client"
description: >
With Config.Insecure=true, getClient() builds a *http.Client with
TLSClientConfig.InsecureSkipVerify=true.
fields:
config: "&Config{Insecure: true}"
before:
description: Force n.client = nil so getClient() builds the client respecting Config.Insecure.
mechanism: field-reset
checks:
- checkClientType(&http.Client{})
- checkClientInsecure(true)
Generated output (excerpt)¶
{
name: "existing-client",
before: func(n *WebhookNotifier) {
// ai-hint: field-injection
// Assign n.client = &mockHTTPClient{} before calling getClient().
},
checks: model.CheckNotifier(
checkClientType(&mockHTTPClient{}),
),
},
{
name: "empty-client",
before: func(n *WebhookNotifier) {
// ai-hint: field-reset
// Force n.client = nil to ensure getClient() constructs a new one.
},
checks: model.CheckNotifier(
checkClientType(&http.Client{}),
checkClientInsecure(false),
),
},
{
name: "insecure-client",
config: &Config{Insecure: true},
before: func(n *WebhookNotifier) {
// ai-hint: field-reset
// Force n.client = nil so getClient() builds the client respecting Config.Insecure.
},
checks: model.CheckNotifier(
checkClientType(&http.Client{}),
checkClientInsecure(true),
),
},