Help us shape the future of Go! Take the 2026 Go Developer Survey

workflowtest

package
v0.0.0-...-f96e7c4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 17 Imported by: 0

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

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

func Registry(t testing.TB, extra ...workflow.Definition) *workflow.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

func Run(t *testing.T, newDriver func(t *testing.T) workflow.Driver, opts ...Option)

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

func (fake *Fake) AssertNothingStarted(t testing.TB)

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

func (fake *Fake) Cancel(_ context.Context, name, id string) error

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

func (fake *Fake) Complete[Out any](name, id string, out Out)

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

func (fake *Fake) Fail(name, id string, err error)

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

func (fake *Fake) FailNext(err error)

FailNext makes the next driver call return err. It takes precedence over Fake.FailWith for that call.

func (*Fake) FailWith

func (fake *Fake) FailWith(err error)

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

func (fake *Fake) Result(ctx context.Context, name, id string) ([]byte, error)

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

func (fake *Fake) Signal(_ context.Context, name, id, signal string, v []byte) error

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

func (fake *Fake) Started() []StartCall

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"
}

func (*Fake) Status

func (fake *Fake) Status(_ context.Context, name, id string) (workflow.Status, error)

Status returns the state of the run.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL