Documentation
¶
Overview ¶
Package workflowtest helps test code that starts workflows and checks drivers of the workflow package. It offers two tools.
Fake is a workflow.Driver for unit tests: it records the runs that code starts, signals and cancels, and tests script their outcome with Fake.Complete and Fake.Fail. It never executes workflow bodies.
fake := workflowtest.New()
client := workflow.NewClient(fake)
run, _ := client.Start(ctx, Onboard, "user-42", user)
fake.AssertStarted(t, Onboard, func(in User) bool {
return in.ID == 42
})
fake.Complete(Onboard.Name(), "user-42", "welcome")
out, err := run.Result(ctx)
Run is the conformance suite that every driver must pass. A driver test builds the registry with Registry and hands it to the driver:
func TestConformance(t *testing.T) {
workflowtest.Run(t, func(t *testing.T) workflow.Driver {
driver := memoryworkflow.New(workflowtest.Registry(t))
t.Cleanup(func() {
_ = driver.Stop(context.Background())
})
return driver
}, workflowtest.WithSynctest())
}
Drivers backed by an engine run the suite from a test gated like their other integration tests, without WithSynctest. A driver that cannot run Child or Spawn declares WithoutChild or WithoutSpawn; these require ErrUnsupported and absence of submitted child work rather than silently skipping the cases. All other operations keep the same contract assertions. Child ownership cases independently prestart a signal-blocked target, then require it to survive the joining parent's cancellation and MaxDuration and finish with its original input. Newly created children must instead stop with their parent. Observable work lives in steps with atomic per-run counters, never in replayed orchestration. Native driver tests can additionally observe their engine's join acknowledgement before requesting cancellation. WithoutImmediateStepRetries declares only that completed multi-attempt step policies with zero initial backoff cannot be expressed. The case still runs: it requires ErrUnsupported and zero affected-handler executions. Single-attempt zero-backoff and positive-backoff controls remain required. The workflow entry control also checks that step retries were not replaced by whole-workflow retries. Registry starts an independent recording telemetry service for worker-side extraction. The suite checks serialized start carriers, retained-map ownership, and forwarding to actual child/spawn steps where those operations are supported. Fake records a detached Trace map but never fabricates worker observations.
Index ¶
- func Definitions() []workflow.Definition
- func Registry(t testing.TB, extra ...workflow.Definition) *workflow.Registry
- func Run(t *testing.T, newDriver func(t *testing.T) workflow.Driver, opts ...Option)
- type CancelCall
- type Fake
- func (fake *Fake) AssertCanceled[In, Out any](t testing.TB, w *workflow.Workflow[In, Out], id string)
- func (fake *Fake) AssertNotStarted[In, Out any](t testing.TB, w *workflow.Workflow[In, Out], preds ...func(In) bool)
- func (fake *Fake) AssertNothingStarted(t testing.TB)
- func (fake *Fake) AssertSignaled[In, Out, T any](t testing.TB, w *workflow.Workflow[In, Out], id string, ...)
- func (fake *Fake) AssertStarted[In, Out any](t testing.TB, w *workflow.Workflow[In, Out], preds ...func(In) bool)
- func (fake *Fake) AssertStartedCount[In, Out any](t testing.TB, w *workflow.Workflow[In, Out], n int)
- func (fake *Fake) Cancel(_ context.Context, name, id string) error
- func (fake *Fake) Canceled() []CancelCall
- func (fake *Fake) Complete[Out any](name, id string, out Out)
- func (fake *Fake) Fail(name, id string, err error)
- func (fake *Fake) FailNext(err error)
- func (fake *Fake) FailWith(err error)
- func (fake *Fake) Reset()
- func (fake *Fake) Result(ctx context.Context, name, id string) ([]byte, error)
- func (fake *Fake) Signal(_ context.Context, name, id, signal string, v []byte) error
- func (fake *Fake) Signaled() []SignalCall
- func (fake *Fake) Start(ctx context.Context, name, id string, in []byte, o workflow.StartOptions) error
- func (fake *Fake) Started() []StartCall
- func (fake *Fake) Status(_ context.Context, name, id string) (workflow.Status, error)
- type Option
- type Order
- type Receipt
- type SignalCall
- type StartCall
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Definitions ¶
func Definitions() []workflow.Definition
Definitions returns every workflow and step the suite runs. Register all of them in the driver that is passed to Run. The returned slice is independent; definitions remain immutable shared values. Include them even when a suite option requires rejection of a particular policy.
definitions := workflowtest.Definitions()
registry, err := workflow.NewRegistry(definitions)
if err != nil {
return err
}
_ = registry.Steps()
func Registry ¶
Registry returns the registry of Definitions together with extra, for newDriver to give to the driver under test. It fails the test when the definitions are invalid. Worker registration uses an independently started recording telemetry service so serialized start/child/spawn propagation can be checked. Cleanup stops it.
registry := workflowtest.Registry(t, applicationFlow, applicationStep)
driver := memoryworkflow.New(registry)
t.Cleanup(func() {
if err := driver.Stop(context.Background()); err != nil {
t.Error(err)
}
})
func Run ¶
Run runs the conformance suite against the drivers that newDriver returns. Every driver must run the workflows and steps of Definitions; build its registry with Registry. Without WithSynctest, newDriver is called once with the t of Run, and the subtests run in parallel against that driver. Register cleanup of the driver with t.Cleanup.
The suite uses short durations, up to a few hundred milliseconds, so engines with real clocks can run it. Every run id has a random prefix, so the suite can run again against an engine that keeps its history. WithoutChild and WithoutSpawn require rejection without submission for those operations; they do not relax any other contract assertions. WithoutImmediateStepRetries similarly asserts rejection before affected step execution for zero-initial multi-attempt policies. It does not disable the immediate single-attempt or existing positive-backoff retry controls.
Example:
workflowtest.Run(t, newDriver, workflowtest.WithoutChild(), workflowtest.WithoutSpawn())
Types ¶
type CancelCall ¶
type CancelCall struct {
// Name is the workflow name.
Name string
// ID is the run identifier.
ID string
}
CancelCall is one recorded workflow.Driver.Cancel.
type Fake ¶
type Fake struct {
// contains filtered or unexported fields
}
Fake is a workflow.Driver for unit tests. It records the calls of the code under test and never executes workflow bodies: a run stays Running until the test ends it with Fake.Complete, Fake.Fail, or a cancel. Payloads use the default workflow.JSON codec.
Fake follows the contract of the driver for the conditions that do not need a body: a second Start of an id, finished or not, returns workflow.ErrAlreadyStarted, and unknown runs return workflow.ErrNotFound. Every workflow name is considered registered. Because it cannot execute bodies, Fake does not run the Run suite.
A Fake is safe for concurrent use. Create it with New.
func New ¶
func New() *Fake
New returns an empty Fake.
Examples ¶
fake := workflowtest.New()
client := workflow.NewClient(fake)
run, err := client.Start(ctx, Onboard, "user-42", user)
fake.AssertStarted(t, Onboard, func(in User) bool {
return in.ID == 42
})
func (*Fake) AssertCanceled ¶
func (fake *Fake) AssertCanceled[In, Out any](t testing.TB, w *workflow.Workflow[In, Out], id string)
AssertCanceled fails the test unless run id of workflow w was canceled.
Examples ¶
fake.AssertCanceled(t, Onboard, "user-42")
func (*Fake) AssertNotStarted ¶
func (fake *Fake) AssertNotStarted[In, Out any](t testing.TB, w *workflow.Workflow[In, Out], preds ...func(In) bool)
AssertNotStarted fails the test when a run of workflow w was started whose input satisfies every predicate. Without predicates any start of w fails the test.
Examples ¶
fake.AssertNotStarted(t, Onboard)
func (*Fake) AssertNothingStarted ¶
AssertNothingStarted fails the test when any workflow was started.
func (*Fake) AssertSignaled ¶
func (fake *Fake) AssertSignaled[In, Out, T any](t testing.TB, w *workflow.Workflow[In, Out], id string, signal workflow.Signal[T], preds ...func(T) bool)
AssertSignaled fails the test unless run id of workflow w received signal with a value that satisfies every predicate.
Examples ¶
fake.AssertSignaled(t, Onboard, "user-42", EmailConfirmed, func(token string) bool {
return token != ""
})
func (*Fake) AssertStarted ¶
func (fake *Fake) AssertStarted[In, Out any](t testing.TB, w *workflow.Workflow[In, Out], preds ...func(In) bool)
AssertStarted fails the test unless a run of workflow w was started whose input satisfies every predicate. Without predicates any start of w matches. The input type comes from w, so the predicates are type checked.
Examples ¶
fake.AssertStarted(t, Onboard, func(in User) bool {
return in.ID == 42
})
func (*Fake) AssertStartedCount ¶
func (fake *Fake) AssertStartedCount[In, Out any](t testing.TB, w *workflow.Workflow[In, Out], n int)
AssertStartedCount fails the test unless workflow w was started exactly n times.
Examples ¶
fake.AssertStartedCount(t, Onboard, 2)
func (*Fake) Cancel ¶
Cancel records the call and ends a running run as canceled. A finished run is left as it is.
func (*Fake) Canceled ¶
func (fake *Fake) Canceled() []CancelCall
Canceled returns the recorded Cancel calls in order.
func (*Fake) Complete ¶
Complete ends run id of workflow name with out, encoded like the output of a real run, so workflow.Run.Result returns it. Completing a run that was not started yet scripts it: the later Start returns a run that is already completed. Completing a finished run changes nothing. It panics when out cannot be encoded.
Examples ¶
fake.Complete(Onboard.Name(), "user-42", "welcome")
func (*Fake) Fail ¶
Fail ends run id of workflow name with err. The error crosses the wire encoding like a real failure, so only its kind, message, and permanent mark survive: use workflow.NewError for errors the code under test recognizes with errors.Is. An error that matches workflow.ErrCanceled ends the run as canceled. Like Fake.Complete, it scripts a run that was not started. It panics when err is nil, because a failed run without an error would make Result report success.
Examples ¶
fake.Fail(Onboard.Name(), "user-42", workflow.NewError("card-declined", "declined"))
func (*Fake) FailNext ¶
FailNext makes the next driver call return err. It takes precedence over Fake.FailWith for that call.
func (*Fake) FailWith ¶
FailWith makes every following driver call return err, until FailWith(nil). Failed calls are not recorded.
func (*Fake) Reset ¶
func (fake *Fake) Reset()
Reset forgets every run, recorded call, scripted outcome, and injected failure. Callers blocked in Result stay blocked until their context ends.
func (*Fake) Result ¶
Result blocks until the run is ended by the test or ctx is done. A run that has already ended wins over a done ctx. The output is a copy.
func (*Fake) Signal ¶
Signal records the call. It returns workflow.ErrNotFound for an unknown or finished run.
func (*Fake) Signaled ¶
func (fake *Fake) Signaled() []SignalCall
Signaled returns the recorded Signal calls in order.
func (*Fake) Start ¶
func (fake *Fake) Start(ctx context.Context, name, id string, in []byte, o workflow.StartOptions) error
Start records the call and creates a running run, or one that is already finished when its outcome was scripted. It returns an error wrapping the context error when ctx is already done.
func (*Fake) Started ¶
Started returns the recorded Start calls in order. The returned slice and Options.Trace maps are detached snapshots; editing propagation fields cannot change retained observations or another snapshot. Other record members retain their existing shallow-copy ownership.
calls := fake.Started()
if len(calls) != 0 && calls[0].Options.Trace != nil {
calls[0].Options.Trace["traceparent"] = "local-edit"
}
type Option ¶
type Option func(*options)
Option declares driver capabilities or controls execution of Run. An absent capability is tested for ErrUnsupported, not silently skipped; all remaining operations must satisfy the ordinary workflow contract.
options := []workflowtest.Option{workflowtest.WithoutImmediateStepRetries()}
workflowtest.Run(t, newDriver, options...)
func WithSynctest ¶
func WithSynctest() Option
WithSynctest runs every subtest inside a testing/synctest bubble, so sleeps, delays, and timeouts take no real time. Use it for drivers that live in the process and use the time package, such as the memory driver. Because goroutines of the driver must belong to the bubble, Run then calls newDriver once per subtest, inside the bubble, and subtests run one after another. Never use it with a driver that talks to an external engine.
func WithoutChild ¶
func WithoutChild() Option
WithoutChild declares that the driver cannot run child workflows. The suite checks ErrUnsupported and absence of a newly submitted child instead of Child success/error semantics, and checks rejection when the target already exists. Ownership scenarios still require the independent target to remain running and finish with its original input after rejection; no test assumes a child that the driver has explicitly refused to submit can be canceled. Facade self-ID validation and every Spawn assertion remain enabled.
Example:
workflowtest.Run(t, newDriver, workflowtest.WithoutChild())
func WithoutImmediateStepRetries ¶
func WithoutImmediateStepRetries() Option
WithoutImmediateStepRetries declares that the engine cannot express a completed per-call step policy with Attempts > 1 and Initial == 0. The suite requires ErrUnsupported and zero affected-handler executions, not a skipped case, a substituted positive delay, or retries of the whole workflow. Single-attempt zero-backoff and positive-backoff cases remain required. Register every Definitions entry even when selecting this option.
workflowtest.Run(t, newDriver, workflowtest.WithoutImmediateStepRetries())
func WithoutSpawn ¶
func WithoutSpawn() Option
WithoutSpawn declares that the driver cannot spawn detached workflows. The suite requires ErrUnsupported and no submitted child rather than omitting the Spawn case. Child behavior remains required unless WithoutChild is also set.
Example:
workflowtest.Run(t, newDriver, workflowtest.WithoutSpawn())
type Order ¶
type Order struct {
// Item is copied into the receipt summary by the typed conformance flow.
//
// Example:
//
// order := workflowtest.Order{
// Item: "tea",
// }
// item := order.Item
Item string
// Qty is doubled to produce the receipt's total during the typed test.
//
// Example:
//
// order := workflowtest.Order{
// Qty: 4,
// }
// quantity := order.Qty
Qty int
}
Order is the input of the typed test. It exercises object decoding independently of the workflow's result type.
Example:
order := workflowtest.Order{
Item: "tea",
Qty: 4,
}
type Receipt ¶
type Receipt struct {
// Summary echoes the input's Item to verify typed result decoding.
//
// Example:
//
// receipt := workflowtest.Receipt{
// Summary: "tea",
// }
// summary := receipt.Summary
Summary string
// Total is twice the input quantity in the suite's typed workflow.
//
// Example:
//
// receipt := workflowtest.Receipt{
// Total: 8,
// }
// total := receipt.Total
Total int
}
Receipt is the output of the typed test. It verifies that the driver returns an object distinct from the input shape.
Example:
receipt := workflowtest.Receipt{
Summary: "tea",
Total: 8,
}
type SignalCall ¶
type SignalCall struct {
// Name is the workflow name.
Name string
// ID is the run identifier.
ID string
// Signal is the signal name.
Signal string
// Value is the encoded value.
Value []byte
}
SignalCall is one recorded workflow.Driver.Signal.
type StartCall ¶
type StartCall struct {
// Name is the workflow name.
Name string
// ID is the run identifier.
ID string
// Input is the encoded input, as produced by [workflow.EncodeValue].
Input []byte
// Options are the start options.
Options workflow.StartOptions
}
StartCall is one recorded workflow.Driver.Start.