* Add initial skill for testing, which is simply Steve's skill (#1) * Add initial skill for testing, which is simply Steve's skill * Rename skill to 'temporal-dev' and update version Updated skill name and version for Temporal Python. * Use claude to merge Steve's, Max's, and Mason's skills. (#2) * Use claude to merge Steve's, Max's, and Mason's skills. Did a review pass using claude's skill devlopment skills * Add missing things from Steve * trigger tweaks * Add in common gotchas from Johann * add simple feedback mechanism (#3) * Change skill name to kebab-case, for compatibility with Amp and Cline (#7) * Clean up references/core/ai-integration.md * Clean up references/core/common-gotchas.md * Clean up references/core/common-gotchas.md * Clean up references/core/determinism.md * Clean up references/core/determinism.md * Update error-reference.md * Update interactive-workflows.md * Clean up patterns.md * Cut shell scripts * Edit troubleshooting.md * remove interceptors for now * remove dynamic workflows * clarify on heartbeating of async activity completions, and prompt it a bit in relation to signals * Improve references/python/advanced-features.md * Use explicit namespace in connect * remove duplicated content from determinism.md, clean up * Improve references/python/data-handling.md * Prefer start_to_close_timeout * don't explicitely provide defaults for retry policies * error-handling.md cleanup * move idempotency patterns to patterns.md * remove multi-param activities * small edits * Unify sandbox stuff into one file * local activities aren't experimental * Clean up references/python/sync-vs-async.md * Cleanup observability.md, remove duplicated search attributes * Cut otel for now * cut a lot of duplicate stuff from python gotchas, address comments * de-duplicate content * Lots of improvements to testing * cleanup to top level of skill (like CLI install instructions), and to top-level of python * Improve patterns.md * clean up ai-patterns.md * Update readme with installation instructions * remove ts directory * De-couple core from python and TypeScript as much as possible * Remove TypeScript hints * add prompting for feedback at startup - wait for ethan on slack channel * shorten url * Update slack channel * Automated pass over on python cleanup & deduplication * Remove multi-patching from Python, since its obvious, dont waste tokens on it. (#34) * Add TypeScript (#31) Adds initial support for TypeScript to the skill --------- Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com> Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> * Fix typos and reference links (#36) * Fix typos and reference links * 2 more typo fixes * quick edit to readme (#37) * Fix saga compensations to run under cancellation protection (#43) When a workflow is cancelled mid-saga, compensations must run in a cancellation-protected scope, otherwise they are immediately cancelled before they can execute. - Python: wrap compensation loop in asyncio.shield() so it runs even when the workflow receives a CancelledError - TypeScript: wrap compensation loop in CancellationScope.nonCancellable() so it runs even when the root scope is cancelled (per official docs: "Cleanup logic must be in a nonCancellable scope") - TypeScript: also fix compensation registration order — register BEFORE calling the activity (was already correct in Python) Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Update readme for public preview (#45) * a few more readme tweaks (#46) * Add MIT License to the project (#47) * Add Go (supersedes other PR) (#38) * progress on go * Go translation workflow completed. * missed a few spots * Manual edits * Address feedback * Add gotcha about anonymous local activities * Sample code for payload converter * clarify sdk protection mechanisms * Setup CODEOWNERS to AI SDK team (#48) * Align version number in SKILL.md and plugin.json. (#49) --------- Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com> Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
6.4 KiB
Go SDK Testing
Overview
The Go SDK provides the testsuite package for testing Workflows and Activities. It uses the testify library for assertions (assert/require) and mocking (mock). The test environment supports automatic time-skipping for Workflows with timers.
Test Environment Setup
Two approaches: struct-based with suite.Suite or function-based with testsuite.NewTestWorkflowEnvironment().
Approach 1: Struct-based (testify suite)
package sample
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"go.temporal.io/sdk/testsuite"
)
type UnitTestSuite struct {
suite.Suite
testsuite.WorkflowTestSuite
env *testsuite.TestWorkflowEnvironment
}
func (s *UnitTestSuite) SetupTest() {
s.env = s.NewTestWorkflowEnvironment()
}
func (s *UnitTestSuite) AfterTest(suiteName, testName string) {
s.env.AssertExpectations(s.T())
}
func (s *UnitTestSuite) Test_MyWorkflow_Success() {
s.env.ExecuteWorkflow(MyWorkflow, "input")
s.True(s.env.IsWorkflowCompleted())
s.NoError(s.env.GetWorkflowError())
}
func TestUnitTestSuite(t *testing.T) {
suite.Run(t, new(UnitTestSuite))
}
Approach 2: Function-based
package sample
import (
"testing"
"github.com/stretchr/testify/assert"
"go.temporal.io/sdk/testsuite"
)
func Test_MyWorkflow(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
env.RegisterActivity(MyActivity)
env.ExecuteWorkflow(MyWorkflow, "input")
assert.True(t, env.IsWorkflowCompleted())
assert.NoError(t, env.GetWorkflowError())
var result string
assert.NoError(t, env.GetWorkflowResult(&result))
assert.Equal(t, "expected", result)
}
You must register all Activity Definitions used by the Workflow with env.RegisterActivity(ActivityFunc). The Workflow itself does not need to be registered.
Activity Mocking
Mock activities with env.OnActivity() to test Workflow logic in isolation.
Return mock values:
env.OnActivity(MyActivity, mock.Anything, mock.Anything).Return("mock_result", nil)
Return a function replacement (for parameter validation or custom logic):
env.OnActivity(MyActivity, mock.Anything, mock.Anything).Return(
func(ctx context.Context, input string) (string, error) {
// Custom logic, assertions, etc.
return "computed_result", nil
},
)
Match specific arguments:
env.OnActivity(MyActivity, mock.Anything, "specific_input").Return("result", nil)
When using mocks, you do not need to call env.RegisterActivity() for that Activity. The mock signature must match the original Activity function signature.
Testing Signals and Queries
Use RegisterDelayedCallback to send Signals during Workflow execution. Use QueryWorkflow to test query handlers.
func (s *UnitTestSuite) Test_SignalsAndQueries() {
// Register a delayed callback to send a signal after 5 seconds
s.env.RegisterDelayedCallback(func() {
s.env.SignalWorkflow("approve", SignalData{Approved: true})
}, time.Second*5)
s.env.ExecuteWorkflow(ApprovalWorkflow, input)
s.True(s.env.IsWorkflowCompleted())
s.NoError(s.env.GetWorkflowError())
}
Query a running Workflow (must be called inside RegisterDelayedCallback or after ExecuteWorkflow):
s.env.RegisterDelayedCallback(func() {
res, err := s.env.QueryWorkflow("getProgress")
s.NoError(err)
var progress int
err = res.Get(&progress)
s.NoError(err)
s.Equal(50, progress)
}, time.Second*10+time.Millisecond)
QueryWorkflow returns a converter.EncodedValue. Use .Get(&result) to decode the value.
For "Signal-With-Start" testing, set the delay to 0.
Testing Failure Cases
func (s *UnitTestSuite) Test_WorkflowFailure() {
// Mock activity to return an error
s.env.OnActivity(MyActivity, mock.Anything, mock.Anything).Return(
"", errors.New("activity failed"))
s.env.ExecuteWorkflow(MyWorkflow, "input")
s.True(s.env.IsWorkflowCompleted())
err := s.env.GetWorkflowError()
s.Error(err)
var applicationErr *temporal.ApplicationError
s.True(errors.As(err, &applicationErr))
s.Equal("activity failed", applicationErr.Error())
}
env.GetWorkflowError() returns the Workflow error. Use errors.As(err, &applicationErr) to check the error type. Mock activities returning errors to test Workflow error-handling paths.
Replay Testing
Use worker.NewWorkflowReplayer() to verify that code changes do not break determinism. Load history from a JSON file exported via the Temporal CLI or Web UI.
package sample
import (
"testing"
"github.com/stretchr/testify/assert"
"go.temporal.io/sdk/worker"
)
func Test_ReplayFromFile(t *testing.T) {
replayer := worker.NewWorkflowReplayer()
replayer.RegisterWorkflow(MyWorkflow)
err := replayer.ReplayWorkflowHistoryFromJSONFile(nil, "my_workflow_history.json")
assert.NoError(t, err)
}
Export history via CLI: temporal workflow show --workflow-id <id> --output json > history.json
Replay from a programmatically fetched history:
func Test_ReplayFromServer(t *testing.T) {
// Fetch history from the server
hist, err := GetWorkflowHistory(ctx, client, workflowID, runID)
assert.NoError(t, err)
replayer := worker.NewWorkflowReplayer()
replayer.RegisterWorkflow(MyWorkflow)
err = replayer.ReplayWorkflowHistory(nil, hist)
assert.NoError(t, err)
}
Activity Testing
Test Activities in isolation using TestActivityEnvironment. No Worker or Workflow needed.
func Test_MyActivity(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestActivityEnvironment()
env.RegisterActivity(MyActivity)
val, err := env.ExecuteActivity(MyActivity, "input")
assert.NoError(t, err)
var result string
assert.NoError(t, val.Get(&result))
assert.Equal(t, "expected_output", result)
}
ExecuteActivity returns (converter.EncodedValue, error). Use val.Get(&result) to extract the typed result. The Activity executes synchronously in the calling goroutine.
Best Practices
- Register all Activities used by the Workflow with
env.RegisterActivity(), unless you mock them withenv.OnActivity() - Use mocks to isolate Workflow logic from Activity implementations
- Test failure paths by mocking Activities that return errors
- Use replay testing before deploying Workflow code changes to catch non-determinism errors
- Use unique task queues per test when running integration tests
- Call
env.AssertExpectations(s.T())inAfterTestto verify all mocks were called