Temporal Testing with Go: a roadmap
Why this matters, in 30 seconds
A Workflow can sleep for 30 days, retry an Activity for hours, wait for a Signal that comes next week, and must keep replaying the same way after every deploy. You can't test that by running it and waiting. The Go SDK gives you four tools:
- The Workflow test environment runs your Workflow in the test process with a mock clock: 30 days pass in milliseconds, and Activities, Child Workflows and Nexus Operations can be mocked.
- The Activity test environment runs one Activity with a fake context, so you can check Heartbeats and cancellation.
- The Workflow replayer replays real Event Histories against your new code and catches a non-deterministic change before it breaks running Workflows.
testsuite.StartDevServerstarts a real Temporal Service for end-to-end tests.
Each has traps that only show up when you run it: a 3-second idle timeout that panics, retry delays that don't grow, a replayer that logs scary stack traces, a TEMPORAL_DEBUG=false that turns debug mode on. This guide covers them.
The roadmap
Stage 1: Foundations (core concepts, must-know)
| # | Chapter | You'll be able to… |
|---|---|---|
| 01 | Testing strategy | Tell unit, integration and end-to-end tests apart, pick the right Go tool for each question, and write a first Workflow test. |
| 02 | The Workflow test environment | Set up a testify suite (or plain testing), run a Workflow, read its result and error, and keep the two timeouts apart. |
Stage 2: Daily use (the tests you'll write every day)
| # | Chapter | You'll be able to… |
|---|---|---|
| 03 | Mocking Activities and Child Workflows | Mock or override Activities and Child Workflows, inject failures, test retries, and assert on calls. |
| 04 | Testing Activities | Test an Activity in isolation with fake dependencies, Heartbeats, resume-from-heartbeat and cancellation. |
| 05 | Time, Signals, Queries and Updates | Skip 30 days in milliseconds, send Signals and Updates mid-run, and Query the Workflow's state. |
Stage 3: Advanced (Nexus and determinism)
| # | Chapter | You'll be able to… |
|---|---|---|
| 06 | Mocking Nexus Operations | Test a Workflow that calls another team's Nexus Service, with sync and async mocks or a fake Service. |
| 07 | Replay testing | Replay golden and live histories in go test and CI, and catch a breaking change before deploy. |
Stage 4: Production
| # | Chapter | You'll be able to… |
|---|---|---|
| 08 | End-to-end tests with a dev server | Start a real Service from TestMain, run a real Worker with fake Activities, and isolate each test. |
| 09 | Debugging Workflows | Step through a Workflow without tripping the deadlock detector, and debug a production failure by replaying it locally. |
Core concepts vs. implementation details
| Core concepts: understand these first | Implementation details, options & sub-features: learn when needed |
|---|---|
| Unit, integration, end-to-end; mostly integration tests (01) | Four Go tools and when to use which (01) |
| The Workflow runs in-process with a mock clock, no server (02) | testify suite vs plain testing, SetTestTimeout vs SetWorkflowRunTimeout (02) |
| Real Activities run unless you mock them (02, 03) | Matchers, .Once/.Times/.After/.Run, OnWorkflow, Assert* (03) |
| An Activity can be tested alone (04) | Heartbeat listener, SetHeartbeatDetails, cancellation, local Activities (04) |
| Time skips when nothing runs (05) | RegisterDelayedCallback, SetStartTime, Signal-With-Start, UpdateWorkflow callbacks (05) |
| Nexus calls can be mocked like Activities (06) | Operation references, async tokens, fake Services (06) |
| Replay proves new code matches old histories (07) | Golden histories, cmd/replaycheck, quiet logger (07) |
| End-to-end runs a real Service, in real time (08) | DevServerOptions, TestMain, per-test Task Queues, -short (08) |
| The deadlock detector vs the debugger (09) | TEMPORAL_DEBUG, DeadlockDetectionTimeout, CLI show/stack, replay under a debugger (09) |
Which test do I need?
text
Does my Workflow take the right steps, branches, results? → Workflow test env, Activities mocked → 02, 03
What happens when an Activity fails or is retried? → mock returning errors, then success → 03
Is my Activity code itself right?
├── Pure logic (parsing, mapping) → plain Go test, no Temporal → 04
└── Uses its context (Heartbeats, cancellation, GetInfo) → Activity test env → 04
Does it wait / time out / react to Signals, Queries, Updates? → mock clock + RegisterDelayedCallback → 05
Does it call a Nexus Operation? → OnNexusOperation or a fake Service → 06
Will my change break Workflows already running? → replay golden + live histories in CI → 07
Is everything wired (registration, Task Queues, converters)? → StartDevServer + real Worker → 08
A test hangs in the debugger with "Potential deadlock"? → TEMPORAL_DEBUG=1 (any non-empty value) → 09
A production Execution failed and I want to step through it? → download its history, replay locally → 09Go API cheat sheet
| I want to… | Go API | Chapter |
|---|---|---|
| Create a Workflow test environment | var ts testsuite.WorkflowTestSuite; env := ts.NewTestWorkflowEnvironment() (or embed testsuite.WorkflowTestSuite in a testify suite) | 02 |
| Run the Workflow and read the outcome | env.ExecuteWorkflow(fn, args…), env.IsWorkflowCompleted(), env.GetWorkflowResult(&v), env.GetWorkflowError() | 02 |
| Raise the 3s idle timeout | env.SetTestTimeout(d) (wall clock) | 02 |
| Cap Workflow time | env.SetWorkflowRunTimeout(d) (mock clock) | 02, 05 |
| Mock an Activity | env.OnActivity(act, mock.Anything, …).Return(result, err) or .Return(func(…) (…, error)) | 03 |
| Mock a Child Workflow | env.OnWorkflow(child, mock.Anything, …).Return(result, err) | 03 |
| Check mocks were used | env.AssertExpectations(t), env.AssertActivityNumberOfCalls(t, "Name", n) | 03 |
| Test one Activity | env := ts.NewTestActivityEnvironment(); env.RegisterActivity(a); v, err := env.ExecuteActivity(a, args…) | 04 |
| See Heartbeats / resume from one | env.SetOnActivityHeartbeatListener(…), env.SetHeartbeatDetails(progress) | 04 |
| Cancel an Activity under test | env.SetWorkerOptions(worker.Options{BackgroundActivityContext: ctx}), then cancel() | 04 |
| Do something at a given Workflow time | env.RegisterDelayedCallback(func(){…}, d) | 05 |
| Signal, Query, Update | env.SignalWorkflow(name, arg), env.QueryWorkflow(name), env.UpdateWorkflow(name, id, &testsuite.TestUpdateCallback{…}, args…) | 05 |
| Set the start time | env.SetStartTime(t); read it with env.Now() | 05 |
| Mock a Nexus Operation | env.OnNexusOperation(service, nexus.NewOperationReference[In, Out](name), in, mock.Anything).Return(…) | 06 |
| Complete an async Nexus mock | env.RegisterNexusAsyncOperationCompletion(service, op, token, result, err, delay) | 06 |
| Replay a history | r := worker.NewWorkflowReplayer(); r.RegisterWorkflowWithOptions(fn, …); r.ReplayWorkflowHistory(logger, hist) | 07 |
| Replay from a CLI export | temporal workflow show -w ID -o json > h.json, then r.ReplayWorkflowHistoryFromJSONFile(logger, "h.json") | 07 |
| Replay a live Execution | r.ReplayWorkflowExecution(ctx, c.WorkflowService(), logger, ns, workflow.Execution{ID: …}) | 07, 09 |
| Start a real Service in tests | srv, err := testsuite.StartDevServer(ctx, testsuite.DevServerOptions{…}); c := srv.Client(); defer srv.Stop() | 08 |
| Step through Workflow code | TEMPORAL_DEBUG=1 in the debugger's environment | 09 |
About the code samples
- Each chapter is a self-contained module named
github.com/acme/shop. Every Go block starts with// file: <path>so you can paste it into that path. - All samples were compiled,
gofmt-checked andgo vet-checked against Go SDK v1.50.0 (go.temporal.io/sdk) withgithub.com/stretchr/testify, and every test was run withgo testand passes. Output shown in the chapters is real output. - Chapter 08 starts its own dev server with
testsuite.StartDevServer(Temporal CLI 1.9.1, Server 1.32.0); chapters 07 and 09 fetched real histories from one. - Not verified: the Delve breakpoint walk-through and the VS Code
launch.jsonin chapter 09 (the debugger couldn't run in the sandbox used for checking). The replay test they rely on is verified.
Notes from checking the docs against the SDK
- One source link is broken.
…/best-practices/debuggingactivitiesis a 404; the page is…/best-practices/debugging. Most of its testing content duplicates the Testing page. - Go has no time-skipping test server. The page says to use "the test server" for end-to-end and integration tests "because the test server supports skipping time". In Go, time skipping exists only in the in-process Workflow test environment, which is not a server.
StartDevServerruns in real time (01, 08). TEMPORAL_DEBUG=falseturns debug mode on. The SDK checksos.Getenv("TEMPORAL_DEBUG") != "", so any non-empty value (true,false,0) disables deadlock detection and raises the test idle timeout to 24h. An explicitworker.Options.DeadlockDetectionTimeoutwins over it (09).- The 3-second idle timeout panics. A real Activity running longer than 3s of wall clock makes the test panic ("test timeout: 3s, workflow stack: …"), not fail. Mock it or call
SetTestTimeout(02). SetTestTimeouton the Activity environment only applies toExecuteLocalActivity.ExecuteActivityalways uses a fixed 10-minute timeout, so an Activity that never returns hangs the test, despite the docs saying the timeout can be set "in the Workflow or Activity environment" (04).- A forgotten Signal fails instantly. A Workflow waiting for a Signal that never comes completes at once with "deadline exceeded (type: ScheduleToClose)": the mock clock jumps to the 10-year default run timeout (02).
- Retries behave differently. In the test environment, retry delays don't grow with
BackoffCoefficient, and an unsetMaximumAttemptsbecomes 10 (a real Service retries forever) (03). - "The framework will validate the signature" of a replacement function, but only when the Activity is called: a wrong signature is an ordinary, retryable Activity error, not a panic at setup (03).
- Missing Child Workflow crashes the test binary. An unregistered, unmocked Child Workflow (or a Nexus handler Workflow) panics from a goroutine
recovercan't catch. A missing Activity fails the Workflow cleanly (03, 06). - Heartbeat listener is throttled. Three quick
RecordHeartbeatcalls reached the listener once. The docs' sample counts heartbeats and works only because it sends one (04). - Cancelled Activity error. The docs' cancellation test only checks
s.Error(err). The error is a retryable*temporal.ApplicationError"context canceled":temporal.IsCanceledErroranderrors.Is(err, context.Canceled)are both false (04). - Querying too early panics with a nil pointer dereference, as the docs warn (05).
- Nexus mock errors lose their text. Plain errors and
nexus.HandlerErrorfreach the Workflow as "handler error (…)"; onlynexus.NewOperationFailedErrorkeeps its message. Mocks ignore the Endpoint name (06). ReplayWorkflowHistory(nil, …)is not silent. The godoc says nil "defaults to the noop logger"; the code uses the default logger, which prints "ERROR Workflow panic" and a stack trace for every failed replay. Pass a logger over anio.Discardhandler (07). The Workflows guide's replay example had the same wrong comment; it's fixed.- A running Execution can pass replay if its history stops before the changed code, so replay a mix of old and new, open and closed Executions (07).
DevServerOptions.SearchAttributesregisters names and types only; the values are ignored (08).- The docs' Query example links to
go.temporal.io/temporal/internal, an old module path; the method isTestWorkflowEnvironment.QueryWorkflowingo.temporal.io/sdk/testsuite.
Sources
Encyclopedia: no page on testing.